REFORGE

1977 / PUBLIC DOSSIER

Atari 2600

A chaptered hardware reference with research coverage, visible citation status, and a separate production record.

CHAPTERS
10
PRODUCTION RECORD
174 PUBLIC FINDINGS
SOURCE STATUS
EXTERNAL REFERENCES LINKED

Audience: Senior systems engineers implementing a static binary-to-C lifter for 6507 ROMs paired with hand-written TIA / RIOT / mapper runtime libraries. This document is not an emulation primer. It documents the quirks that bite static recompilation specifically, the places where naive recursive descent collapses, and the side-effect modelling thresholds at which real commercial games begin to function.

Conventions used throughoutwhen the Stella source and the Stella Programmer's Guide (SPG, Steve Wright, 12/03/79) disagree on cycle counts, the Stella source wins for behavior modelling and SPG wins for design intent. Both are explicitly cited.

CHAPTER 01

1. Power-on Through First Rendered Frame

1.1 6507 reset sequence

The 6507 is a pin-reduced NMOS 6502 die (28-pin DIP, A0–A12 only; no IRQ, no NMI, no SYNC, no RDY brought out on most cart designs). Reset behavior is identical to the NMOS 6502 because the core is the same.

  • Reset takes 7 cycles. The first two are internal microcode. Cycles 3–5 are fake stack pushes — the address bus is driven to $01FD, $01FE, $01FF in turn but the R/W line is forced to read, not write. Cycles 6–7 fetch the reset vector from $FFFC (low) and $FFFD (high). See pagetable.com — "Internals of BRK/IRQ/NMI/RESET on a MOS 6502".
  • A, X, Y are undefined on real hardware. Stella's M6502.cxx initializes them to 0 by convention so runs are reproducible; real silicon reflects metastable latches. See NESDev wiki — CPU power-up state.
  • Stack pointer S is decremented three times by the reset microcode (same path as BRK/IRQ, writes converted to reads). On hardware that powered up with S=$00, this yields S=$FD after reset. The initial SP is itself undefined on NMOS: NESDev forum puts it bluntly: "A 6502 has no boot state. Even in a cold boot you can not rely upon anything having a fixed value."
  • Processor flags P: Only the I flag is set by reset. The D flag is undefined on NMOS — this is why every well-written reset routine opens with SEI; CLD.

Reset assertion to first opcode fetch7 CPU cycles (≈ 5.9 µs at 1.193 MHz NTSC).

1.2 RIOT RAM ($0080–$00FF, 128 bytes) at power-on

The hardware does not clear RAM. The 6532 RIOT contains 128 bytes of static SRAM; at power-up the cells settle to whatever pattern the latches resolved to as Vcc rose — effectively random with strong die-dependent biases.

Stella's default is to zero-fill RAM, but the randomize_ram option toggles a deterministic-but-nonzero pattern designed to expose games that depend on specific uninitialized values (see Stella source repository). A static-recomp runtime that hard-zeros RAM at boot will hide bugs in lifted games that happened to assume specific uninitialized values. Adopt Stella's randomization policy by default.

This is why every commercial game opens with the equivalent of CLEAN_START. From the DASM macro file at github.com/munsie/dasm/.../macro.h, the macro revision history records: "1.03 23/JUN/2003 — CLEAN_START macro added — clears TIA, RAM, registers" (current file is at rev 1.07, 19/JAN/2020). The macro itself, by Andrew Davie:

MAC CLEAN_START
SEI
CLD
LDX #0
TXA
TAY
.CLEAR_STACK
DEX
TXS
PHA
BNE .CLEAR_STACK    ; loop $00-$FF; SP ends at $FF, A=X=Y=0
ENDM

The clever piecePHA writes A=0 to $01FF, $01FE, … working backwards through the stack page, which mirrors RIOT RAM at $0080–$00FF, and simultaneously writes through TIA mirrors at $00–$3F (A12=0, A7=0). One 256-iteration loop zeroes RAM, the stack image, and every TIA write register.

1.3 First-frame timing budget

After CLEAN_START (~270 cycles), most games drop into the VERTICAL_SYNC macro

MAC VERTICAL_SYNC
LDA #$02
STA WSYNC        ; finish current line
STA VSYNC        ; VSYNC on
STA WSYNC
STA WSYNC
STA WSYNC        ; 3 lines of VSYNC
LSR
STA VSYNC        ; VSYNC off (A=0)
ENDM

Cold boot to first visible scanline3 (VSYNC) + 37 (VBLANK) = 40 scanlines × 76 = 3,040 CPU cycles of "blanking before pixels," exclusive of init. First "rendered frame" reachable ≈ 3,300 cycles after reset (≈ 2.8 ms). A static-recomp runtime will not see meaningful TIA write traffic until VBLANK ends and the kernel loop begins.

CHAPTER 02

2. Frame-Loop Anatomy ("Kernel Pattern")

2.1 NTSC frame structure

PhaseScanlinesCyclesNotes
VSYNC3228VSYNC bit 1 set, 3× WSYNC
VBLANK372,812VBLANK bit 1 set; game-state logic
Visible19214,592Kernel — TIA writes track beam
Overscan302,280VBLANK bit 1 set; audio update, etc.
Total26219,9121 frame ≈ 16.68 ms @ 1.193 MHz

Per SPG §3.1, each scanline is 228 TIA color clocks = 76 CPU cycles (3:1; TIA is the master 3.58 MHz colorburst clock, the 6507 runs at /3). Each scanline begins with 68 color clocks of horizontal blank (HBLANK) followed by 160 visible color clocks. See Stella Programmer's Guide (HTML).

2.2 WSYNC ($02)

Writing any value to WSYNC strobes RDY low to the 6507, halting the CPU until the next leading edge of horizontal blank (SPG §3.2). Skipping WSYNC is safe only when you've cycle-counted the kernel to exactly 76 cycles per line; the moment you slip a single cycle, the beam position drifts and any subsequent RESPx strobe positions the sprite at a different column.

2.3 RESPx + HMOVE positioning

The canonical positioning subroutine (Andrew Davie / R. Mundschau, RandomTerrain review cycle):

PosObject SUBROUTINE
sta WSYNC          ; 00  align to start of scanline
sec                ; 02
.divideby15
sbc #15            ; 04  consume the necessary time
bcs .divideby15    ; 06/07  at 11/16/21/26/31/36/41/46/51/56/61/66
tay                ; 13
lda fineAdjustTable,y ; 5 cycles guaranteed (page crossing)
sta HMP0,x
sta RESP0,x        ; coarse position; pixel ≈ 6502_cycle * 3
rts

Three things bite static recompilation here

  1. The subtract loop's cycle count IS the position. A lifter that strength-reduces or unrolls the loop changes the timing and breaks every sprite.
  2. The page-crossing dummy fetch (lda fineAdjustTable,y) is deliberately +1 cycle. A linker that moves the table to a page-aligned address skips the cycle and shifts all sprites by 3 pixels.
  3. **STA RESP0,X is a 4-cycle indexed write with the side effect at the last cycle.** The C runtime must commit the RESPx strobe at the same intra-instruction cycle position real hardware does.

2.4 RESP0 strobe offset

When RESP0 is strobed at TIA color-clock position X (measured from start of scanline, including the 68-clock HBLANK), the player position counter is set such that the first pixel of the player appears at column X + offset. The community argued +4 vs +5 for years. Andrew Towers' TIA Hardware Notes describes the counter as latching one clock late due to strobe propagation, and Stella's TIA.cxx models the player emission as beginning +5 color clocks after the strobe (during the visible portion with no HMOVE active); +4 for missile and ball.

The disagreement between SPG ("object will begin its serial graphics at the time of a horizontal line at which the reset address occurs", reading as +0) and Stella's +5 is a documentation gap in SPG, not a Stella bug. Use +5 for player, +4 for missile/ball, mirroring Stella. The Stella debugger's Pixel Pos field reflects this convention, as confirmed in the AtariAge thread on "Fine player positioning: Stella 'Pixel Pos' value is different from P0 Pos. at RESP0".

2.5 HMOVE blank stripe ("the comb")

From Andrew Towers, TIA Hardware Notes: *"If an HMOVE is initiated immediately after HBlank starts (which is the case when HMOVE is used as documented), the [HMOVE] signal is latched and used to delay the end of the HBlank by exactly 8 CLK… The extra HBlank time shifts everything except the Playfield right by 8 pixels, because the position counters will now resume counting 8 CLK later than they would have without the HMOVE.

This is also the source of the HMOVE 'comb' effect; the extended HBlank hides the normal playfield output for the first 8 pixels of the line."*

Runtime obligation

/* On any scanline where HMOVE was strobed during HBLANK
* (specifically, within the first ~3 CPU cycles after WSYNC release):
*   1. Force pixels 0..7 of visible scanline to background color = 0 (black)
*   2. Shift effective object emission point by +8 color clocks for that line
* Late HMOVEs (after HBLANK has ended) — the illegal HMOVE — produce partial
* combs and stuff clock pulses into mid-line counters; this is what Cosmic
* Ark's starfield exploits. */

The illegal-HMOVE class is per-instruction-precise and required for Cosmic Ark and Stay Frosty rendering. The Stella 3.0 release notes record: *"Illegal HMOVEs are now handled correctly, resulting in improvements to many ROMs (thanks to Wilbert Pol for many ideas and code for these improvements). All HMOVE emulation 'cheats' were removed; the emulation is now cycle-exact in this area.

Improved emulation of the Cosmic Ark 'starfield effect', also used in Stay Frosty; the emulation now looks very accurate."* For tier-1/tier-2 milestones, illegal HMOVEs can be stubbed; for full compatibility they must be modelled.

2.6 Score kernel vs. play-field kernel

The playfield kernel is the inner loop running for 192 lines (or 2 × 96, or 4 × 48 with line-doubling), updating GRP0/GRP1/PF0/PF1/PF2 each scanline. The score kernel is the bottom-of-screen 10–20-line band where CTRLPF.D1 (SCORE bit) is set so each playfield half takes its color from COLUP0/COLUP1 — score digits drawn as PF1/PF2 patterns then appear in player-0 and player-1 colors. Ships/lives indicators use GRP0/GRP1 with VDELP0/VDELP1=1 so each graphics register has a one-line shadow latch, allowing two updates per two-line band without flicker.

For static recompilation the score-kernel pattern is highly stereotyped: the STA PF1 ; STA PF2 ; STA PF1 ; STA PF2 cadence over a 10-line tail of the visible kernel is a reliable score-band detector for automated invariant checks (§10.3).

CHAPTER 03

3. Attract Mode → Gameplay Transition

3.1 Demo-loop detection idioms

"No input yet" detection in 1981-era code is uniformly one of

  • TIM64T countdown: write a value to TIM64T ($0296), poll INTIM ($0284) until it underflows.
  • SWCHA edge-detect: read SWCHA ($0280), XOR against the prior-frame snapshot in zero page; nonzero means a stick moved.
  • INPT4/INPT5 polling: read $003C/$003D. Bit 7=1 means no press (active-low, internal pull-up).

3.2 SWCHB ($0282) console-switch bits

From SPG §4.1

BitSwitch0 means1 means
D7P1 DifficultyAmateur (B)Pro (A)
D6P0 DifficultyAmateur (B)Pro (A)
D5(unused)
D4(unused)
D3TV TypeB/WColor
D2(unused)
D1GAME SELECTpressednot pressed
D0GAME RESETpressednot pressed

Active-low for both buttons. The LSR ; BCS NotReset idiom is universal — see SpiceWare's "Let's Make a Game" Step 8. Unused bits D2/D4/D5 read back as whatever was last written if SWBCNT configures those bits as output — no$2k6 specs records: "the three unused bits could be configured as output, the respective SWCHB bits can be then used to store 3 bits of custom read/writeable data (as done by Combat)."

3.3 Indirect-JMP-via-zero-page dispatch

The static-recomp killer. Asteroids uses JMP (mE3) at $D00B (computerarcheology.com Asteroids disassembly). The target byte $E3 is overwritten by the kernel state machine itself — STA $E3 #$83 at $D01B and $D07C, STA $E3 #$C0 at $D09A, etc. — producing a 4-target dispatch at minimum.

Static disassembly cannot resolve this without abstract interpretation of the writers' value sets. The realistic answer: identify every STA zp whose target is later read by JMP (zp), collect the constant set written across both banks, and emit a C switch over those constants with a fallthrough trap. Asteroids is tractable because every observed STA $E3 is immediate-mode literal; games that compute the dispatch arithmetically require manual annotation.

CHAPTER 04

4. 6507-Specific Behaviors vs. Textbook 6502

4.1 13-bit address bus consequences

The 6507 brings out only A0–A12. From no$2k6 specs: "All memory is mirrored in steps of 2000h." Concretely:

  • Cartridge area is the 4 KB window $1000–$1FFF (A12=1). Also appears at $3000, $5000, $7000, $9000, $B000, $D000, $F000. The reset vector at $FFFC/$FFFD is read through the $F000 mirror, which is why nearly every commercial ROM places its reset code with ORG $F000 even though it physically lives at cart offset $0000.
  • TIA: A12=0, A7=0 — 16 mirrors of 64-byte register block in the 4 KB low half.
  • RIOT RAM: A12=0, A9=0, A7=1 — 128 bytes at $0080, mirrored at $0180 (the stack page).
  • RIOT I/O: A12=0, A9=1, A7=1 — $0280–$029F.

There is no IRQ pin on the 6507 in standard cart wiring; the input is not routed. There is no NMI pin (the die has the pad but the package doesn't). A lifter can safely refuse to emit IRQ/NMI dispatch code, but must still handle BRK because BRK is reachable from RAM-resident code in some games.

4.2 JMP indirect page-boundary bug

JMP ($XXFF) reads the low byte of the target from $XXFF and the high byte from $XX00 (NOT $(XX+1)00). The 6507 has this NMOS bug. From masswerk.at 6502 instruction set reference: "For example if address $3000 contains $40, $30FF contains $80, and $3100 contains $50, the result of JMP ($30FF) will be a transfer of control to $4080 rather than $5080." Asteroids' JMP (mE3) is safe (low byte $E3 is well clear of any $xxFF boundary), but a general lifter must emulate the bug exactly when the indirect operand's low byte is $FF.

4.3 BRK without IRQ wiring

BRK fires the standard 6502 microcodepushes PC+2 and P|0x10 (B flag set in pushed copy), then reads the IRQ vector from $FFFE/$FFFF. On a cart without IRQ logic, $FFFE/$FFFF is just whatever the cart ROM has there. Most carts place a BRK trap or simply set $FFFE/$FFFF = $F000 so a stray BRK restarts the game.

The Pole Position BRK trick (qotile.net advanced programming guide): *"Pole Position puts the stack pointer over the RESxx registers and then does a BRK. There are three write cycles in a BRK instruction, so the three position registers for the objects that make up the road in PP, get accessed in three consecutive cycles.

This is how PP managed to get the road to meet so closely in the horizon."* This requires emulating BRK as actually writing P,PCH,PCL to whatever the stack page maps to — for static recompilation, lift BRK as a multi-cycle write sequence to $0100+SP, decrement SP×3, then read ($FFFE).

4.4 Undocumented opcodes used by commercial games

NMOS 6502 illegal opcodes are stable in their NMOS form. Useful subset known to appear in Atari 2600 commercial and homebrew ROMs — Andrew Davie comments on pagetable.com: "As a 'modern' 2600 programmer, I make use of the undefined opcodes when applicable; LAX and SAX are quite common; DCP is handy."

Opcode(s)MnemonicOperationFlags
A7 B7 AF BF A3 B3LAXA,X = MN,Z
87 97 8F 83SAXM = A AND X (no flags affected)
4BALRA = (A AND imm) >> 1N,Z,C
6BARRA = ((A AND imm) >> 1) with C-rotate-inN,Z,C,V
07 17 0F 1F 1B 03 13SLOASL M; A = A OR MN,Z,C
47 57 4F 5F 5B 43 53SRELSR M; A = A XOR MN,Z,C
27 37 2F 3F 3B 23 33RLAROL M; A = A AND MN,Z,C
67 77 6F 7F 7B 63 73RRAROR M; A = A + M + CN,Z,C,V
C7 D7 CF DF DB C3 D3DCPM = M − 1; CMP A,MN,Z,C
E7 F7 EF FF FB E3 F3ISCM = M + 1; SBC A,MN,Z,C,V
9F 93AHX/SHAM = A AND X AND (high(addr)+1) — UNSTABLE
9ESHX/SXAM = X AND (high(addr)+1) — UNSTABLE
9CSHY/SYAM = Y AND (high(addr)+1) — UNSTABLE
0B 2BANCA = A AND imm; C = NN,Z,C
EBSBC*Alias of legal $E9 SBCN,Z,C,V
02 12 22 32 42 52 62 72 92 B2 D2 F2JAM/KILHalt CPU

The SHA/SHX/SHY "AND with (high byte of address + 1)" semantics are page-crossing dependent. From the Preterhuman wiki, undocumented opcodes list: "The observation of the function 'AND with the high byte of the argument + 1' (from Adam Vardy's list) seems to be correct." When the indexed address crosses a page, the high byte fed into the AND is the new high byte.

Commercial Atari 2600 titles known to use illegals: I could not locate a definitive published per-title catalogue from a primary source. Community statements (Davie on pagetable; the comp.sys.apple2 thread) confirm LAX, SAX, and DCP appear in homebrew and "some" commercial titles. Treat as: implement LAX/SAX/DCP/SLO/SRE/ALR/ARR/RLA/RRA/ISC at minimum, AHX/SHX/SHY at low priority, JAM as a runtime fatal.

Cosmic Ark's starfield is NOT an illegal opcode — it's a TIA hardware bug (illegal-HMOVE class) exploited at the chip level. Wikipedia: *"According to Fulop, the game was created entirely as a feat of technical one-upmanship: to show off the impressive background starfield effect to Activision programmers David Crane and Bob Whitehead.

The starfield effect uses a bug in the Atari 2600 hardware."* (Wikipedia cites Rob Fulop's 14 April 2008 blog post "Making Crane Cry — The Origin of Cosmic Ark" and Scott Stilphen's Digital Press interview with Fulop as primary sources.)

CHAPTER 05

5. TIA Quirks Beyond the Register Table

5.1 Position counters

Five position counters (P0, P1, M0, M1, BL). They are continuously clocked during the visible portion of every scanline at the color-clock rate, wrapping modulo 160. When the counter wraps through zero, the corresponding object opens its graphics emission window. Writing to RESPx/RESMx/RESBL forces the counter to a state that will next wrap at "now + offset" where the offset is +5 (player), +4 (missile/ball) per §2.4. The counter is not zeroed; it is set to a value producing the wrap at the desired moment, so subsequent HMOVE adjustments remain relative to the current counter.

5.2 HMOVE comb

See §2.5. The 8-pixel left-edge stripe and the +8-clock object shift on the line where HMOVE is strobed during HBLANK.

5.3 Mid-frame RESPx re-strobe

Pitfall! and Asteroids' attract-mode multi-ship animation rely on re-strobing RESPx mid-frame. The position counter resets on every strobe, so a kernel can draw a player at column 30 across lines 50–80, then re-strobe RESP0 at column 100 in the HBLANK before line 81 and the same GRP0 byte renders a second copy at column 100 for lines 81–120. The lifter must NOT coalesce or eliminate RESPx writes — every one is a side-effecting hardware operation tied to the cycle at which it executes.

5.4 Collision latches

Eight 1-bit latches read at $30–$37. Latches set during pixel rendering when two objects emit pixels at the same color clock; cleared by any write to CXCLR ($2C).

ReadRegBit 7Bit 6
$30CXM0PM0/P1M0/P0
$31CXM1PM1/P0M1/P1
$32CXP0FBP0/PFP0/BL
$33CXP1FBP1/PFP1/BL
$34CXM0FBM0/PFM0/BL
$35CXM1FBM1/PFM1/BL
$36CXBLPFBL/PF(no missile)
$37CXPPMMP0/P1M0/M1

Lower 6 bits of each latch register read as data-bus residue (no storage); most games mask to bits 6/7. Stella's changelog records repeated "Fixes for collision corner cases (during HBlank)" — collisions during HBLANK are an edge case where two counters' wrap points overlap. For tier-1/tier-2 model collisions only during the visible portion (color clocks 68–227); revisit if a specific ROM depends on HBLANK collisions.

5.5 Audio

Per SPG §11 and no$2k6 specs

  • AUDCx (5-bit channel-control, $15/$16): 0=silence, 1=4-bit poly, 4=div-by-2 pure, 6=div-by-31 pure, 12=div-by-6 pure, 15=poly5+poly4. Common pure-tone codes: 4, 6, 12.
  • AUDFx (5-bit divider, $17/$18): output frequency ≈ 30 kHz / (AUDFx + 1) for pure modes; AUDF=0 ≈ 30 kHz, AUDF=31 ≈ 937 Hz.
  • AUDVx (4-bit volume, $19/$1A): 0 = true silence (output transistor pulled off), 1 = lowest, 15 = max. Hard cliff at AUDV=0 — a linear-volume runtime will produce audible hum.
CHAPTER 06

6. RIOT 6532 Quirks

6.1 INTIM countdown

From no$2k6 specs

  • Writes to TIM1T ($0294), TIM8T ($0295), TIM64T ($0296), T1024T ($0297) load the 8-bit counter AND select the prescaler (1, 8, 64, 1024 CPU cycles per tick).
  • "The timer is decremented once immediately after writing." Write $0A to TIM64T, INTIM reads $09 next cycle.
  • While non-zero, the timer decrements at the prescaler rate. On underflow, the timer decrements every CPU cycle and INSTAT ($0285) bit 7 goes high. Reading INSTAT clears bit 6 but not bit 7; writing a new TIMxT value clears bit 7.

A lifter needs a true cycle-counted timer model — not frame-grain. Many state machines (Davie's "Advanced Timeslicing", RandomTerrain review cycle) measure exact post-VBLANK time remaining by reading INTIM after underflow.

6.2 SWCHA, SWCHB internal pull-ups

Both ports have internal pull-ups; unconnected input pins read 1, active-low for both joystick directions and console switches. SWCHA bit layout: D7=P0 up, D6=P0 down, D5=P0 left, D4=P0 right, D3–D0 same for P1. Pressed direction reads 0; released reads 1.

Static-recomp failure modea runtime returning 0 from any unmapped MMIO read makes every game think every joystick direction is held simultaneously. Default SWCHA = $FF, SWCHB = $3F (RESET and SELECT not pressed, both difficulties = amateur, color mode).

6.3 DDR registers

SWACNT ($0281) and SWBCNT ($0283) configure pins as input (0) or output (1). Rarely written by commercial code. Combat writes SWBCNT to expose the three unused SWCHB bits as readable scratch. Default both DDRs to 0 (input); model writes only on observation.

6.4 RAM mirroring and stack-share

128 bytes, visible at $0080–$00FF (primary), $0180–$01FF (stack page — same physical bytes), and repeated at every $0400 boundary up through the 8 KB address space.

Stack and zero-page variables share 128 bytes. A program that uses $80–$BF for variables and lets the stack grow from $FF fails catastrophically if SP descends below $C0. The lifter MUST model this aliasing — do not allocate two C arrays for "zero page" and "stack page".

CHAPTER 07

7. Bank-Switching Formats

All hotspot addresses below from Kevin Horton's "Mostly Inclusive Atari 2600 Mapper / Selected Hardware Document" (v1.00, 03/04/12). Cross-checked against 6502ts/resources/stella/mappers.txt.

7.1 DEEP coverage

F8 (Atari 8K)

  • ROM: 2 × 4K banks. Hotspots: $1FF8 (bank 0), $1FF9 (bank 1).
  • Any access — read, write, RMW — triggers. Horton: "ANY kind of access will trigger the switching."
  • Power-on bank: hardware undefined; Stella's CartF8.cxx initializes to the second bank. Practical convention: dual-bank stubs at identical addresses in both banks.
  • Bank-switch-while-PC-in-window: instruction containing the hotspot access completes from the old bank (opcode and operand already latched); the next opcode fetch is from the new bank. The dual-bank trampoline below MUST exist at the same address in both banks.
  • RMW on hotspot (e.g., INC $1FF8): triggers the switch in the middle of the instruction's read-modify-write sequence. No commercial game does this; trap in lifter.
  • Detection: Stella autodetects by scanning for LDA $1FF8/LDA $1FF9/BIT $1FF8/BIT $1FF9 patterns in the image; an 8 KB ROM with these patterns and no other hotspot patterns is F8.
  • Marquee: Asteroids (CX2649), Defender (CX2609), Space Invaders (CX2632), Pac-Man (CX2646), Battlezone (CX2681).

F6 (Atari 16K)

  • ROM: 4 × 4K banks. Hotspots: $1FF6/$1FF7/$1FF8/$1FF9 (banks 0/1/2/3).
  • Edge cases identical to F8.
  • Marquee: Crystal Castles, Donkey Kong Junior, Star Wars: Empire Strikes Back, Pete Rose Baseball, Solaris, Crossbow.

F4 (Atari 32K)

  • ROM: 8 × 4K banks. Hotspots: $1FF4–$1FFB.
  • Marquee: Fatal Run.

FA (CBS RAM Plus, 12K + 256 RAM)

  • ROM: 3 × 4K banks. Hotspots: $1FF8/$1FF9/$1FFA.
  • RAM: 256 bytes — write port $1000–$10FF, read port $1100–$11FF. RMW on this RAM is broken by design (read reads from the write port = open bus). No commercial game does RMW on FA RAM; trap in lifter.
  • Marquee: Omega Race, Tunnel Runner, Mountain King.

FE (Activision 8K, "Subroutine Controlled Bank Select")

  • ROM: 2 × 4K banks. Hotspot is not an address — the mapper sniffs the data bus for $20 (JSR) and $60 (RTS) opcodes plus stack accesses in $0100–$01FF, latching bit 5 of the value pushed/pulled. From Horton: "It watches for 20 (JSR) and 60 (RTS), and accesses to 100-1ff … latch D5. This is the NEW bank we need to be in."
  • Design constraint: every JSR/RTS target must be in either $Fxxx (bit 5 of high byte = 1) or $Dxxx (bit 5 = 0).
  • Lifter implementation: Track JSR/RTS by opcode (decoded anyway). At each JSR site emit bank = (target_high_byte >> 5) & 1; at each RTS return emit the same using the pulled high byte. Horton's emulator-cheat: "A13 can be used to simply select which 8K bank to be in." If the runtime sees A13 on every fetch, JSR/RTS sniffing is unnecessary.
  • Marquee: Decathlon, Robot Tank, prototype Thwocker — the only three FE titles.

E0 (Parker Bros 8K)

  • ROM: 8 × 1K banks. Four 1K slots:
    • $1000–$13FF via hotspots $1FE0–$1FE7 (banks 0–7)
    • $1400–$17FF via $1FE8–$1FEF
    • $1800–$1BFF via $1FF0–$1FF7
    • $1C00–$1FFF fixed to bank 7 (last 1K of ROM — the boot/handler slot)
  • Power-on: slot 3 is hardwired so the reset vector at $FFFC/$FFFD comes from the last 1K. Slots 0–2 start indeterminate; the fixed slot initializes them.
  • Marquee: Frogger II, Star Wars: Empire Strikes Back, Montezuma's Revenge, Tooth Protectors (the only non-PB E0 title).

E7 (M-Network 16K + RAM)

  • Slots: $1000–$17FF selectable (ROM banks 0–6 via $1FE0–$1FE6, or 1K RAM via $1FE7); $1800–$19FF 256-byte windowed RAM (4 banks via $1FE8–$1FEB); $1A00–$1FFF fixed to last 1.5K ROM.
  • RAM: 1K at lower slot (write port $1000–$13FF, read port $1400–$17FF); 256B windowed at $1800–$19FF (write $1800–$18FF, read $1900–$19FF).
  • Per Kevin Horton's sizes.txt, BurgerTime is the only cart to actually activate the 2 K of RAM available in the E7 scheme. A frequently repeated community claim that BurgerTime copies subroutines to RAM during VBLANK has not been independently sourced to a disassembly; treat as community lore pending verification.
  • Marquee: BurgerTime, Bump 'N' Jump, Masters of the Universe.

3F (Tigervision 8K)

  • Slots: $1000–$17FF selectable 2K (any of up to 4 ROM banks; homebrew extends to 256); $1800–$1FFF fixed to last 2K.
  • Hotspot: write to $003F — any value selects bank N. Overlaps TIA write range but $3F is unused on TIA; reads still hit TIA mirrors. Bank-select is by STA $3F only.
  • Static-recomp implication: the only common mapper whose hotspot lives in zero-page TIA mirror space. A lifter that elides stores to TIA write space as no-ops will break Tigervision. Special-case: every store decoded to $003F must dispatch to the bank-switch handler.
  • Marquee: Espial, Miner 2049er, Polaris, Springer, River Patrol.

7.2 Bank-switch-while-PC-in-window — the dual-bank trampoline

; Bank 0 at $1FF6:
StartBank0  STA $1FF9        ; operand fetched from OLD bank,
; bank flips at end of access
; Bank 1 at $1FF9:
EntryBank1  JMP NewBankCode  ; opcode fetched from NEW bank

The store completes (4-cycle absolute STA) with the old bank still mapped — operand bytes have already been fetched. The very next opcode fetch comes from the new bank. The post-bank-switch entry label must exist at the same address in both bank versions.

7.3 LDA-from-hotspot and RMW-on-hotspot

LDA $1FF8 is the typical bank-select form. Fetches opcode, operand-low, operand-high, then reads $1FF8; the bank flips at this read. The byte loaded into A is undefined in practice (games never use it). INC $1FF8 (5-cycle RMW) flips the bank mid-instruction, causing the write phases to land in the new bank. No commercial game does this; lifter must trap.

7.4 STUB enumeration

MapperDescription / DetectionMarquee
DPC (Pitfall II)10 K ROM + custom chip with 8 fetcher counters (5 video/data, 3 audio). DPC regs at $1000–$107F; ROM at $1080–$1FFF. Detect via 10240-byte image + DPC register pattern. Implementation = separate co-processor model.Pitfall II: Lost Caverns
DPC+ (Harmony homebrew)ARM coprocessor on cart running custom Thumb code. Detect via ARM image signature. Refuse-unsupported.various homebrew
CTY (Chetiry homebrew)Tetris clone with extended audio engine. Refuse-unsupported.Chetiry
CV (Commavid 2K + 1K RAM)No bankswitching. RAM at $1000–$17FF (read port $1000–$13FF, write $1400–$17FF); ROM at $1800–$1FFF.Magicard, Video Life
UA (UA Ltd 8K)2 × 4K banks. Hotspots $0220 (bank 0), $0240 (bank 1) — below ROM area, in I/O mirror space.Pleiades (proto)
SARA / F8SC / F6SC / F4SC (Atari Superchip)128 bytes of RAM: read port $1080–$10FF, write port $1000–$107F. Detect by Stella's heuristic scan for $1000-$10FF as RAM.Dig Dug (F8SC), Millipede (F6SC), Crystal Castles (F4SC)
F0 (Megaboy 64K)16 × 4K banks. Hotspot $1FF0 increments bank counter (wraps at 16). Selecting code keeps poking $1FF0, comparing a per-bank tag byte.Megaboy (Dynacom — only commercial F0 title)
CHAPTER 08

8. Asteroids (CX2649, 1981, Atari, F8, 8192 bytes)

The reference ROM is an 8192-byte F8 cartridge. Authoritative SHA-1 hashes are recorded in Stella's stella.pro properties file under "Asteroids" — query github.com/stella-emu/stella for the specific entry. Multiple ROM variants exist (Rev 1, Rev 7; PAL builds).

The community disassembly at computerarcheology.com maps bank 0 at $D000–$DFFF and bank 1 at $F000–$FFFF. The disassembly header notes: "The code is designed to run in 2 4K bank-switch mode or flat 8K mode (in 12K map ... D000 to FFFF)."*

8.1 Boot

F8 powers up with the second (upper) bank mapped — bank 1 in the disassembly's labelling, containing $F000-$FFFF source. The reset vector is therefore read from bank 1's $FFFC/$FFFD. The bank-0 view begins at $D000 with:

D000: 4C DA F9    JMP $F9DA      ; jump to reset vector in Bank 1
; (also valid for flat 8K layout)

That is, the bank-0 reset stub immediately transfers to the bank-1 reset entry Reset1 at $F9DA. Real init lives in bank 1: CLEAN_START–style zero pass, RNG seed prep ($B9 is bumped every frame for odd/even task-switching per the disassembly header), then drop into the main attract-mode entry. Zero-page allocation per the disassembly's RAM comment:

  • $B9 — frame parity counter
  • $E4/E5, $E6/E7 — pointers into $D0xx/$D1xx (kernel jump-target pages)
  • $EC–$F3 — asteroid picture pointers
  • $F4/F5 — digit-font pointer
  • $F6–$FB — six score digit values
  • $FC–$FF — assembled digit-image bytes for the score kernel

8.2 Visible-frame kernel (bank 0, $D003)

D003: 85 02       STA WSYNC
D005: 85 2A       STA HMOVE
D007: 85 1C       STA GRP1
D009: 86 05       STX NUSIZ1
D00B: 6C E3 00    JMP (mE3)      ; INDIRECT DISPATCH via zero-page $E3

The indirect at $D00B is dispatched through zero-page byte $E3, which is overwritten by the kernel state machine itself at multiple sites in bank 0 alone:

PCBytesEffectNext-dispatch target
$D019A9 83LDA #$83$D083
$D01B85 E3STA $E3
$D07AA9 83LDA #$83$D083
$D07C85 E3STA $E3
$D098A9 C0LDA #$C0$D0C0
$D09A85 E3STA $E3

The bank-0 visible-frame state machine cycles between approximately 4 immediate-mode targets ($D083, $D0C0, and adjacent labels reachable via fallthrough). Static-recomp resolution: collect every STA $E3 immediate-mode constant across both banks, emit a runtime switch over the constant set at the JMP (mE3) site with a fallthrough trap. The constant set is small (≤ 8 addresses) and fully recoverable by static analysis because every observed writer uses immediate-mode literals.

8.3 Bank-1 ↔ Bank-0 swap pattern

The disassembly contains explicit "Switch to Bank 1" and "Switch to Bank 0" stubs near the top of each bank. Canonical F8 idiom:

; In bank 0:
SwitchTo1   LDA $1FF9            ; hotspot read — bank 1 mapped after this
JMP EntryInBank1     ; opcode fetched from bank 1

Both banks contain SwitchTo0/SwitchTo1 stubs at identical addresses, ensuring the JMP's opcode fetch lands on valid code regardless of which bank is mapped during the transition. In Asteroids these swaps cluster around: score rendering (bank 0 holds digit font + score kernel; bank 1 holds main game logic + reset), collision sound-table lookups, and the attract-mode demo path (bank 1 demo AI alternating with bank 0 rendering kernel).

8.4 Attract mode → gameplay

Per the Atari CX2649 Asteroids manual: "ASTEROIDS includes 66 game variations for 1 or 2 players." GAME RESET (SWCHB bit 0 → 0) starts gameplay; GAME SELECT (SWCHB bit 1 → 0) cycles through the 66 variations. The attract demo plays a simulated game with internal joystick AI; the demo terminates on a GAME RESET low-edge.

8.5 Known indirect-JMP-via-zero-page sites

PCInsnTarget byteRecovered target set
$D00BJMP (mE3)$E3{ $D083, $D0C0, … } — fully immediate-mode
(additional sites in bank 1's Vertical Blank and Reset (Bank 1) regions — see the linked disassembly for the bank-1 dump.)

For Asteroids the dispatch table is fully recoverable; for games that compute dispatch arithmetically, manual annotation is unavoidable.

8.6 Variants — sidebar

Rev 1 (the common dump) and Rev 7 (later revision with bug fixes around saucer AI and demo timing) share kernel structure; only minor differences in attract-mode timing and variation table. Treat as the same execution-flow class; emit a per-variant constant table if needed. Out of scope further.

CHAPTER 09

9. Static-Recompilation Hazards (DEEP)

9.1 Self-modifying code

ROM is not writable on stock carts. True SMC of code-bearing cart bytes is impossible. But

  • RAM-resident routines exist in FA, E7, and SARA games. Per Kevin Horton's sizes.txt, only BurgerTime among E7 carts actually activates the 2 K RAM, and at least some E7/FA titles execute small subroutines copied into RAM (the often-cited claim that BurgerTime does so during VBLANK is unsourced community lore — confirm with a disassembly before relying on it).
  • The Supercharger (AR mapper) is entirely RAM; every commercial Starpath release is SMC by construction. The Supercharger bootloader in Kevin Horton's mappers.txt is a self-modifying CMP whose operand is overwritten before execution.

For F8/F6/F4 carts the lifter can assume code immutability. Detect RAM-execution by tracking PC against $0080–$00FF at lift time; if no reachable PC lands in RAM, no SMC handling is needed.

9.2 PC-relative idioms defeating recursive descent

  • ROM jump tables: LDA tbl_lo,X / STA $E3 / LDA tbl_hi,X / STA $E4 / JMP ($E3). Recursive descent never enters tbl_lo/tbl_hi (data). The lifter must read the table at lift time and emit an N-way switch.
  • PHA-PHA-RTS dispatch: push (target-1) >> 8, push (target-1) & 0xFF, RTS. Common in state machines. Pattern-match LDA #imm / PHA / LDA #imm / PHA / RTS and emit the goto directly.
  • JMP (zp) with stored constants: tractable when stores are immediate-mode (Asteroids); requires abstract interpretation otherwise.

9.3 The .byte $2C / .byte $24 skip trick

The single most pervasive code-flow hazard. From the Dominant Amber byte-saving writeup on hackaday.io:

lda ScreenSaverPosition
cmp #65
beq DontIncScreenSaverPosition
inc ScreenSaverPosition
.byte $2C            ; BIT abs — eats next 2 bytes as operand
DontIncScreenSaverPosition
dec ScreenSaverOffset

When fallthrough reaches $2C, the next byte is BIT abs (3-byte). BIT executes against the address formed by the next two bytes — which are the opcode + operand of DEC ScreenSaverOffset. So the dec is skipped (executed as a no-op data fetch). Entering from the beq target executes the dec normally. $24 (BIT zp) skips 1 byte the same way. $04/$0C (undocumented NOP zp / NOP abs) likewise. Pole Position uses BIT abs in its road-rendering setup.

Lifter implications:

  1. Mark BOTH the BIT abs operand bytes (as a non-instruction read) AND any branch target landing inside that BIT (as a real instruction entry). Single-pass linear sweep fails immediately.
  2. Lift both interpretations as separate basic blocks even though they occupy overlapping byte ranges. Two C functions: one entered via fallthrough through the BIT, one entered via branch into the DEC.
  3. Detect: any branch landing in the middle of a previously-decoded instruction is a guaranteed skip trick. Restart decoding from the landing point.

9.4 Cycle-counting taxonomy

Routine classCycle-exact?Tolerance
Sprite positioning (RESPx)Exact0 cycles
HMOVE strobe + post-windowExact0 cycles (must land in first ~3 cyc post-WSYNC)
WSYNC releaseExact (RDY-forced)0 cycles
Score kernel PF writesExact0 cycles
2-line / 1-line kernel bodyExact0 cycles
VBLANK game logicLooseMust fit in 2,812 cyc
OverscanLooseMust fit in 2,280 cyc
Attract-mode state machineLooseTIM64T-driven
Collision processingLooseRead-driven

The C runtime should expose cycle accounting as a per-instruction 64-bit counter increment; the TIA model computes cycles % 228 to derive color-clock-within-line and dispatches HBLANK/visible behavior. Do not attempt frame-grain cycle accuracy — sprite positioning will fail.

9.5 What breaks if WSYNC/VSYNC are faked

If the runtime no-ops WSYNC

  • Sprites won't position (RESPx timing decoupled from beam).
  • Score won't render (depends on mid-line PF1/PF2 writes).
  • Anything beam-synced fails.

But state-machine progression survives if (a) TIM64T fakes the prescaler in some unit (simulated CPU cycles), (b) VSYNC/VBLANK still mark frame fences, (c) inputs default to pull-up high. "Fake timer + frame fence + ignore beam" gets games to attract mode and through demo loops. Useful as the tier-1 milestone for headless invariant testing.

9.6 Other landmines

  • Mirror decoding: A lifter MUST decode every memory access through the full A12/A9/A7 rules before dispatching to a C runtime hook. Don't pattern-match on literal $0002 — pattern-match on the decoded register. The same TIA register is reachable through dozens of address values.
  • Reads from TIA write registers: TIA write addresses ($00–$2C) are write-only by SPG. Stella returns open-bus / address-bus residue. BIT $02 (a WSYNC strobe via BIT) incidentally reads the write address; modelling: return data-bus residue, not zero.
  • Open-bus on cart range: Outside the 4K cart window, reads return open bus. Within mirrored cart space, the cart drives the bus. Model per address-decode rules.
CHAPTER 10

10.1 Minimum side-effect-accurate register set

To get a typical commercial title to gameplay (not just attract mode), the C runtime MUST model

Reg(s)Side effect
WSYNC ($02)Stall CPU until next color-clock-position-0 (HBLANK leading edge)
VSYNC ($00)Bit 1: frame fence; rising edge starts new frame
VBLANK ($01)Bit 1: blanks output; bit 6: latch INPT4/5; bit 7: dump INPT0–3 to ground
HMOVE ($2A)Strobe; if within first ~3 cyc post-HBLANK start, comb effect
RESP0/1, RESM0/1, RESBL ($10–$14)Strobe; set position counter for +5/+4/+4 offset
HMP0/1, HMM0/1, HMBL ($20–$24)Latch motion delta
GRP0/1 ($1B/$1C)Latch with VDELP shadow
PF0/1/2 ($0D–$0F)Latch — re-read each color clock
COLUP0/1/PF/BK ($06–$09)Latch
CTRLPF ($0A)Reflect, score, priority, ball size
NUSIZ0/1 ($04/$05)Player width and copy count
INPT4/5 ($3C/$3D)Bit 7 = button, latched per VBLANK bit 6
SWCHA/B ($0280/$0282)Joystick + console switches; pull-up defaults $FF / $3F
TIM64T ($0296), INTIM ($0284), INSTAT ($0285)Timer + underflow flag
AUDC0/1, AUDF0/1, AUDV0/1 ($15–$1A)Accept writes; audio output can stub initially
CXCLR ($2C)Clear all 8 collision latches
CXM0PCXPPMM ($30–$37)Read collision latches

The RIOT timer model is the unsung load-bearing piece — without it, the attract-mode state machine never advances on most games.

10.2 Common false-GREEN failure modes

These are "the test harness says PPU writes look active but the game state never advances" symptoms. Each is a specific runtime bug:

  1. Timer never fires → TIM64T not modeled or INTIM not decrementing → attract state machine wedged.
  2. SWCHA reads 0 → Pull-ups not modeled → every direction reads as "held"; demo or input filtering loops forever.
  3. INPT4/INPT5 read 0 (not $80) → Same pull-up issue → button reads as held; title screen skips or hangs.
  4. Frame counter doesn't increment → VSYNC frame-fence missing → RNG sequence identical every "frame."
  5. Collision latches don't clearCXCLR ignored → death loop after first collision.
  6. WSYNC doesn't stall → CPU free-runs through kernel → RESPx at wrong cycle → sprites at wrong column or off-screen.
  7. HMOVE comb not modeled → Visible artifact only; tier-3 priority.
  8. TIM64T pre-decrement not modeled → Game writes $0A, reads $0A back expecting $09 → off-by-one in state-machine timing.

10.3 Per-game smoke-test invariants

Beyond clean exit, lifted ROMs should pass per-title invariants

  • Score increment: Trace PF0/PF1/PF2 writes in the score-band kernel; assert digit values change after a staged input sequence.
  • Player sprite column change: Trace RESP0 strobe column over 30 frames with steady left-stick; assert monotonic leftward column.
  • Audio on collisions: Stage a collision; assert AUDC0 or AUDC1 write within 8 frames.
  • Asteroids attract-mode CRC delta: Snapshot the assembled framebuffer at 60-frame intervals; the rotating-ships animation produces continuous monotonic CRC change (~1 revolution per 2 seconds). Static CRC = VSYNC fence missing or kernel not running.
  • Bank-flip count: For F8 carts count $1FF8/$1FF9 accesses per frame. Asteroids in attract does 2–6 swaps/frame; zero means the bank decoder is broken.

10.4 Suggested milestone gates

  • Tier 1 (headless attract): TIM64T + SWCHA/SWCHB pull-ups + VSYNC frame fence + INPT4/5 pull-ups. Most games reach attract mode and animate state-machine RNG.
  • Tier 2 (rendered attract): T1 + cycle-accurate WSYNC stall + RESPx +5/+4 offsets + GRP0/GRP1 + VDELP shadowing + PF0/PF1/PF2 + reflect bit + HMOVE comb. Asteroids visibly animates rotating ships.
  • Tier 3 (gameplay): T2 + collision latches + full audio register model + illegal opcode subset (LAX/SAX/DCP/SLO/SRE/ALR/ARR/RLA/RRA/ISC) + illegal-HMOVE class. Most non-custom-chip commercial titles run.
  • Tier 4 (custom carts): FA/E7/SARA RAM ports + FE stack-watch bank switch + Supercharger emulation + DPC fetchers. Out of MVP scope.

Public sources

Production ledger

CONFIRMEDATARI2600-F181

CLASS A LANDED: MID-SCANLINE GRP/COLUP REPLAY IN THE SCANLINE RENDERER

THE DEFECT (Verdict-6 dominant visual class, 5+ ROMs): the 48px/6-digit score kernel sets NUSIZx=3-copies-close and rewrites GRPx between copies so each 8px copy shows a different digit. Our renderer drew each line from ONE end-of-line snapshot state — all three copies rendered the same bytes ("three sets of scores that are just one").

BEING VERIFIEDATARI2600-F180

CLASS C ASPECT FIX: ATARI WINDOW NOW PRESENTS 2× HORIZONTAL PIXEL ASPECT (Verdict-6 "squished left-right", universal)

A TIA color clock is ~2 scanline-height units wide; square-pixel presentation rendered every title tall-and-narrow (owner verdict 6 on Adventure, true corpus-wide; the old code's width4 WINDOW with width-logical content just pillarboxed square pixels). Fix in the Atari ppu_init (ppu_c.py): SDL_RenderSetLogicalSize(2width, height) with window (2width3, height*3) — the 160-wide TIA framebuffer texture is stretched horizontally by RenderCopy;…

CONFIRMEDATARI2600-F179

COMBAT FRAME-0 DEATH ROOT-CAUSED AND FIXED

SYMPTOM: fresh builds of Combat die before frame 1 completes — [DISPATCH] t=0x1CC8 → (cache-dependent) [DISPATCH-MISS] or phantom sub_001CC8 execution → dispatch t=0x0001 → OUTER-LOOP-NATURAL-EXIT, 0/1800 frames. June-12 fresh corpus had Combat L2/1800f; every corpus "refresh" since classified from stale artifacts (F178), so the break stayed invisible until the first honest rebuild.

BEING VERIFIEDATARI2600-F178

EVIDENCE-PIPELINE DEFECT (F102 CLASS)

MECHANISM: prove_loop's template-regen step (09ag-10 lineage) constructs its CEmitter with only console_id/architecture/screen dims. The platform.c template emits the headless stats call only when "ppu_collect_stats" in self._ppu_source — with ppu_source empty, every regen emitted the no-hook stub.

CONFIRMEDATARI2600-F176

REFERENCE-TRACE INSTRUMENT PART (a) LANDED

WHAT LANDED: (1) emitter_c.py reforge_debug_frame (the all-console per-frame debug hook, active when reforge_debug_active — any --watch flag) now emits [ZP] f=N + 512 hex chars = the first 256 bytes of cpu->memory, the SAME window the frame CRC hashes — so a CRC mismatch between runs can immediately be decomposed into the exact cells. One line per frame, debug runs only, zero release-build cost.

CONFIRMEDATARI2600-F175

TRAJECTORY VERIFIED CONSISTENT

This record documents the conclusion: TRAJECTORY VERIFIED CONSISTENT; SINGLE-CELL DIFFERENTIAL TOOLKIT EXHAUSTED. The $95 fine-pos evolves in clean -$10 steps ($FA9A wrap store, every 7th odd frame) punctuated by $FDE3/$FDE7 rewrites — which identifies F134's RAM-shift loop as OBJECT-LIST COMPACTION. Every observable cell feeding the deadlock is now either verified-faithful code or self-consistent data; the remaining instrument is a REFERENCE TRACE. Spec…

CONFIRMEDATARI2600-F174

RIOT I/O MIRROR READ DECODE FIXED

Even frames instead write E7=$DF@$F838 (ROM shape page) — the two flicker programs use the SAME cells with different meanings. A4 participates only in timer WRITES ($294-$297).

CORRECTED / SUPERSEDEDATARI2600-F173

TWO-STREAM KERNEL DECODED (F171's page-cursor model corrected)

Exit $11422: LDX #$1F / TXS (SP → TIA ENABL $1F — the per-scanline stack-blit re-arm; the $1C variant lives at the $11313 site) / TAX / INY (SCANLINE ADVANCE) / CPY #89 (band wrap). So the scanline structure is: [stream-A read/advance-or-repeat] → [draw A] → [stream-B read/advance-or-repeat] → [blit re-arm] → INY → CPY #89 → next line.

CONFIRMEDATARI2600-F171

F170's CAUSALITY INVERTED: the DC/DD rebuild runs at frame END (after kernel exit)

This record documents the conclusion: F170's CAUSALITY INVERTED: the DC/DD rebuild runs at frame END (after kernel exit) — its absence on the death frame is a CONSEQUENCE of the spin, not its cause; the walk ALWAYS runs with DC=$00/DD=$09. The merged five-cell tape gives the complete intra-frame program, and the true differential is the OBJECT'S BAND VALUE: bands 14/13/12 render fine, band 11 — the top-zone boundary — kills the walk at cursor step $D4.…

BEING VERIFIEDATARI2600-F170

THE FREEZE MECHANISM, FULLY EVIDENCED

THE TELL: at f=666 the rebuild ran; at f=667 the reset ran ($F5CE/$F5D2) but the REBUILD NEVER FIRED — and from f=668 NOTHING writes DC/DD again (the kernel never yields back to logic). [STUCK-STATE] DC=$00/DD=$09 = the reset values, frozen.

CORRECTED / SUPERSEDEDATARI2600-F169

F168 REFUTED twice over: SP=$1C is Asteroids' stack-on-TIA blit kernel (TXS), not corruption

CAPABILITY LANDED (all-console pattern, atari2600-gated where noted): the emitter now accumulates every masked target routed through a deferred-dispatch emission — the F158 unresolved-branch path AND the F47 trampoline fallback — in CEmitter.deferred_targets; after emit_project the engine unions the set into dispatch_miss_cache.txt (same [DISPATCH-MISS] target=0x%04X (cached) format the Findings-51/52 feedback loop parses).

BEING VERIFIEDATARI2600-F168

FREEZE ROOT CAUSE FOUND: a RUNAWAY JSR CYCLE at f=668 pushes ~113 orphaned stack frames IN ONE FRAME (SP $FF→$1C)…

CAPABILITY LANDED: the platform-template STUCK detector now emits [STUCK-STATE] f a x y sp last_addr last_func mem 83 84 8C DC DD E5 E6 EA C8 to stderr before exit(2) — a STUCK verdict now carries its own forensic snapshot (F158 diagnostics pattern; cells chosen for the Asteroids band loop but harmless everywhere). MECHANISM (fits every prior observation): the ship-death path enters, for the first time, a loop whose JSR'd helper hits an F158 deferred…

BEING VERIFIEDATARI2600-F167

GOAL AC#3 BANKED: F8 bank-switch regions added to the runtime memory map (runtime/memory_map.py

The atari2600 map (_atari2600_memory_map) previously modeled the cart as a flat 4KB ROM region with a passing mention of bank schemes. Added per Findings A2600-F34/F36/F159 with citations inline: (1) f8_hotspots MemoryRegion $1FF8-$1FFA, MemoryRegionType.IO, readable AND writable — on F8 hardware ANY access (LDA/BIT/data fetch or store) to $1FF8 selects bank 0 and $1FF9 selects bank 1; the cells still return ROM bytes on read; mirrors at every 8KB…

BEING VERIFIEDATARI2600-F166

VECTOR SELECTOR DECODED ($111E9-$11200)

Then LDA #0 / INY / CPY #89 / BEQ $11218 (band-loop wrap at 89) / TAX / JMP $11005 (next band). MODEL CORRECTION (F151/F163 revised): the $83+i array holds BAND POSITIONS (vertical Y-coordinates in kernel bands 0-88), not lifetime ages — new entries spawn at #$59=89 (just past the wrap = entering from the bottom edge), and the per-two-frame DEC at $F424 moves the object up one band.

CORRECTED / SUPERSEDEDATARI2600-F165

F164 MODEL CORRECTED: the freeze is a JMP ($E5) KERNEL SELF-CYCLE, not an in-function spin

CORRECTED FREEZE MODEL: from f=668 the dispatch chain still cycles — kernel → JMP ($E5) → kernel — but the selector never matches (TYA's band value vs $83+X ages), so neither the renderer nor the logic segment ever runs again: empty world AND frozen ager are the same symptom. THE DEADLOCK: the ages that would satisfy the match are advanced by the ager, which lives behind the match.

BEING VERIFIEDATARI2600-F164

THE FREEZE IS AN INFINITE LIST-SCAN LOOP, NOT A DISPATCH CYCLE

CORRECTED MODEL: the "display loop" of F162/F163 is not a degenerate dispatch cycle — the CPU is stuck inside ONE C function (sub_0110C0, the same function iter-34's non-TAS natural-exit implicated) in a goto-only infinite loop; frames keep advancing solely because io_tick inside the spinning loop drives the frame clock, and the TIA snapshot state (olive bk, no objects) is whatever the last full frame left behind.

CONFIRMEDATARI2600-F163

prove_loop `--watch` passthrough landed (first TAS+watch capture) and the DEATH SEQUENCE IS ON TAPE

CAPABILITY: prove_loop.py now accepts repeatable --watch ADDR,MODE and forwards them to the exe run — watch evidence under TAS replay, previously impossible (prove_loop.py is not in _HASH_INPUTS, so no regen invalidation). INTERPRETATION: $83 counting $0F→$0B looks like a death/expiry timer; at the $0B transition a state change fires whose post-transition dispatch path loops display-only — the logic re-entry (overscan logic segment of the frame chain)…

BEING VERIFIEDATARI2600-F162

TAS-freeze content identified

Combined with iter-34's logic-stall signature (LFSR churn ceases) and the alive bank-switch counter: the display loop cycles bank-0 kernel frames forever while the bank-1 logic that populates the object list and sprite data never runs again (or runs but produces an empty world). The shape matches the F151-class object-list death, now triggered in the TAS timeline around f≈668 — TAS inputs (rotation/thrust/fire) plausibly get the ship killed near a wave…

BEING VERIFIEDATARI2600-F161

THIRD DISPLAY KERNEL DISCOVERED AND ROOTED ($x10B, vector-swapped at $111EF)

Notable: after the swap ALL logic activity ceased in the watch stream — the LFSR churn (ROL $82 at $FAE9, ~2 writes/frame) stopped — the logic loop stalls when the display chain dies. CORRECTION RECORDED: in Asteroids $82 is an LFSR cell (sub_00FAE2's ROL chain), NOT a game-state variable — do not reuse Pac-Man's RAM map labels.

CONFIRMEDATARI2600-F160

GAMEPLAY KERNEL RUNS: FIRST-EVER ASTEROIDS GAMEPLAY VISUAL

THE DROP MECHANISM (closes the F159 question): engine.py's CFG-seed block correctly forwarded 0x115C/0x1115C (both decoded), but cfg.py's Finding-74 misaligned-leader filter removed 0x1115C from the leader set because it falls inside another decoded instruction's byte range — built before the F8 alias stitch existed, F74 assumed interior-ness is absolute; with deliberately-overlapping alias streams an address can be interior to one alignment AND a…

CONFIRMEDATARI2600-F159

F8 READ-PATH bank switching landed (any access to $1FF8/$1FF9 now flips the bank, matching hardware

Two changes landed (emitter_c.py): (1) HARDWARE-CORRECTNESS — addr_map_read now flips reforge_active_bank on ANY read of $1FF8/$1FF9 (LDA/BIT/data fetches), where Finding 36 had implemented only the write path; the bank flips before the byte fetch so the returned byte comes from the newly selected bank. Affects only F8 ROMs (rom_size > 0x1000); Asteroids is the corpus' only F8 cart.

BEING VERIFIEDATARI2600-F158

DYING INSTRUCTION PINPOINTED AND FIXED

The emitted code at $11155 was if (cond) {{ / branch to unresolved bb_01115C / return; }} — THE DEFECT CLASS: _emit_branch (emitter_c.py ~1415) emitted a bare return; for any taken branch whose target block wasn't decoded, ending the outer dispatch chain silently AND leaking reforge_call_depth (the depth=1 fingerprint). FIX: on atari2600, an unresolved conditional branch now emits `cpu->trampoline_target = (target & 0x1FFF); reforge_call_depth--;…

BEING VERIFIEDATARI2600-F156

The f=313 exit is a DISPATCH-MISS on the GAMEPLAY DISPLAY KERNEL

Evidence chain: (1) F154's fix is confirmed in the regenerated code (the 13 DEC $0285,X sites now emit (0x0285 + cpu->x) unmasked). The dispatcher (game_main.c ~line 1130-1495) is a static switch over lifter-rooted 13-bit entries with reforge_active_bank disambiguation for dual-rooted offsets; 0x1145 has no case, and the default miss handler logs + counts but does NOT set trampoline_target, so the outer loop ends (it also force-stops after 200 total…

CONFIRMEDATARI2600-F155

Analyzer run-length floor landed

The F154 caveat (analyze_log false-GREENed Asteroids' 313-of-1800-frame run) is closed. GenericExitCheck in analyze_log.py — generic, runs for every console — now parses both REFORGE: headless mode -- N frames max (requested) and REFORGE: clean exit after M frames (completed) and emits FAIL when M < N, echoing any NATURAL-EXIT diagnostic lines into section 1.

CONFIRMEDATARI2600-F154

THE TERMINATOR KILLER DECODED

There is NO ager. (2) INDEXED-RMW ZERO-PAGE WRAP — lifter.py's INC/DEC indexed path created the _rmw_addr effective-address vreg with the 6502 register width (W8), so the emitter masked the address ADD with & 0xFF: DEC $0285,X (RIOT I/O space — harmless on hardware, likely never even executed there) became DEC ($85+X) in RAM — and with X=0 that is the list terminator.

BEING VERIFIEDATARI2600-F152

ROOT-CAUSE MECHANISM LOCATED

METHODOLOGY CORRECTION: F140's "all #$E0 loads" enumeration was TRUNCATED by grep pagination (head_limit 60) and missed $FE6F — re-verify enumerations are exhaustive before negative conclusions. THE MECHANISM: every JSR in the emitted sub_00FE16 carries the unwind guard if (trampoline_target) return; if (rts_target != expected) return;.

BEING VERIFIEDATARI2600-F151

TWO-LIST architecture confirmed (list-0 base $83 / list-1 base $8C, tails $DC/$DD)

THE REAPER RUNS EVERY COUNTDOWN FRAME BY DESIGN (the F150 quiescence hypothesis is refuted). So either (a) $FE16 is supposed to write the terminator after the last entry and a diverged branch skips it, (b) the terminator is supposed to come from the $F121 init which is gated behind the very countdown the reaper runs during (circular — unlikely; hardware boots fine), or (c) $FE16 terminates via a path involving the appender ($144B STA $84,X) that our flow…

CONFIRMEDATARI2600-F150

Game-state dispatcher $F0A9-$F101 decoded

REFRAME: nothing here is structurally unreached — the path opens when the counters finish: non-TAS reset releases f=271 → countdown completes ≈f=469; TAS reset f=45 → ≈f=243. ALL our "never executes" watches ran 280 frames — too short for non-TAS, and in the TAS run the F138 reaper wedge fires at f≈261, ANNIHILATING zero page (counters included) BEFORE the countdown completes (f≈243 was close — the wedge and completion race, and the wedge appears to win…

CONFIRMEDATARI2600-F148

$1059-$107A is the GAME-RESET HANDLER (SWCHB-gated)

CORRECTION to F147: the handler does NOT run every frame — the F145 watch counts ($106B ×14, $1074 ×14-15) exactly match the non-TAS autoplay RESET window f=257-270 (released f=271); in the TAS run it runs f=30-45. Each reset-window frame jumps to $FA03; whatever it does, the system lands in the F146 phantom-list state with $BF stuck at $40.

CONFIRMEDATARI2600-F147

Asteroids TAS authored and replay-verified (RESET f=30-45 + three 36-frame fire holds pre-wedge)

tests/tas/atari2600/Asteroids.tas.json committed (35 events; GAME RESET press f=30-45, fire holds f=80-115/150-185/230-265 each spanning the $DE&$1F debounce, then rotation/thrust/fire gameplay cadence; SELECT never pressed). Iter-21: decode the $106B/$1074 routine — entry conditions, the branch (if any) guarding the $40 store, what $40 means (bit6 of $BF), and whether a mislifted branch/flag makes it unconditional; cross-check the mirror copy; then…

BEING VERIFIEDATARI2600-F146

Object lifecycle machinery decoded ($F384-$F3AC)

So in steady state the terminator is MAINTAINED by $F3A8 — no explicit init required once the list is healthy. Iter-20: author tests/tas/atari2600/Asteroids.tas.json (fire press with release, plus maybe a second press; no SELECT), run prove with --tas, and check (a) ENTPROBE first-call sidx (terminator present?), (b) wedge gone?, (c) PPM f=400 nonblack — potentially the first rendered rocks.

CONFIRMEDATARI2600-F145

Init gate fully decoded: $BF passes ($1074 writes $40 per frame)

The alternate path $F63D clears bit6 (AND #$BF) when the input bit is held — classic press-edge + debounce + accept state machine. If that routine is the attract builder and SHOULD be writing real entries + terminator instead of $00, the divergence is inside IT (mislifted compare, wrong input, or an early-exit our runtime takes).

CORRECTED / SUPERSEDEDATARI2600-F144

F143 fix set IMPLEMENTED (watch "rw" parse, 6507 RAM-mirror canonicalization, stack ops through the memory bus)

RESULTS: Asteroids rebuilt + 1800f prove — behavior shifted measurably (376 unique CRCs vs 358; TRAP 101→94; sprite-lines 235→226) but the screen remains black and the ENTPROBE first-call signature is unchanged (f=275, x=$01, sidx=-1): SP never enters the aliasing-sensitive region pre-wedge, so the PHA-strobe hypothesis is REFUTED as the Asteroids root cause (the fix remains correct hardware modeling and a real corpus-wide defect class — any title that…

BEING VERIFIEDATARI2600-F143

TWO runtime-model defects found

Perfect correlation across all six watch runs this goal: every ,w watch recorded writes; every ,rw watch ($96, $83, $BF, $C8) recorded none. The $83 WRITE history must be redone with ,w.

BEING VERIFIEDATARI2600-F142

$83 watch proves the terminator was NEVER WRITTEN

The boot-init block therefore NEVER EXECUTES. In our headless runtime these gates never pass, so the game never initializes its object list, and the f=275 reap walks uninitialized cells straight into the F138 runaway cascade.

BEING VERIFIEDATARI2600-F141

Object reap loop decoded ($FCE7-$FD1D)

THREE paths to DELETE: scan overflow (X reaches $80 — degenerate), $FD0B dead-entry reap, $FD1B status-$30 reap. The F139 first call (f=275, x=$01) therefore arrived via a REAP at index 1 — which requires the scan to have walked PAST [$83] and [$84] as live non-terminator entries.

BEING VERIFIEDATARI2600-F140

Sentinel machinery fully mapped

Static + watchpoint triangulation. Therefore the actual divergence is UPSTREAM in the $FCFF caller's GATE: on hardware that code path only invokes DELETE when the list has entries; our runtime's caller-side condition (object count, game-state flag, or a flag computed from a mislifted instruction) evaluates differently.

BEING VERIFIEDATARI2600-F139

[ENTPROBE] per-call DELETE evidence

New permanent emitter capability: function-entry STATE probes (_entry_state_probe_pcs in emitter_c.py, emitted in the function PREAMBLE so they fire once per CALL, not per loop-iteration of the bb label) printing [ENTPROBE $PC] f x s83 sidx ret — call-time X, the $83 counter, a $E0-sentinel scan over $84-$B3, and the caller's JSR return address read from the stack.

BEING VERIFIEDATARI2600-F138

Asteroids object-list primitives decoded

Both movers decoded from the lifted C (no rebuild). RUNAWAY MECHANISM: if the $84 stream contains no $E0, DELETE's X runs through 255 and the zp,X wrap shifts the ENTIRE zero page down — explaining in one stroke the TIA-window bleed (F134), the $E5/$E6 vector stomp (F137), and total RAM decay.

BEING VERIFIEDATARI2600-F137

Kernel vector $E5/$E6 dies at f≈345

Zero-rebuild diagnosis via the built-in write watchpoints on the standing iter-9 exe (--headless 500 --watch 0xE5,w --watch 0xE6,w, 4,063 parsed events; note: PowerShell >/2> redirection writes UTF-16 — parse with encoding=utf-16). — live display dispatch.

CORRECTED / SUPERSEDEDATARI2600-F136

Flicker hypothesis REFUTED by consecutive-frame journals

Iter-9 evidence closed the F135 fork decisively. Part 1 (no rebuild): pixel-counted ALL existing consecutive PPMs — f=400/401/402/403 and f=1500/1501/1502/1503 are 100% black (0 nonblack pixels each); only f=50 has content (234 px of $C6-red).

BEING VERIFIEDATARI2600-F135

Widened journal (strobes + enables + HMOVE)

F134's fork resolves to (ii) for the dump frames. NEW LEAD from the volume math: ~38 events/frame run-average vs ~12 on each dump frame — frames are HETEROGENEOUS, which is exactly what Asteroids' flicker display would produce (the 2600 port multiplexes many rocks by drawing alternating subsets on alternating frames; a frame can legitimately be object-light).

BEING VERIFIEDATARI2600-F134

Journal writer-PC attribution lands

The TIASnap journal now records the writer PC per event ((cycle,$reg,$val@PC) in [JRNL]; TIASnap.ev_pc + staging + io_write hook pass reforge_last_addr — permanent capability for every console using the TIA journal). Asteroids result, conclusive: EVERY journaled visible-frame TIA write across both dump frames — 321 events — comes from exactly three PCs: $FDE3 (x108), $FDE7 (x105), $FDEB (x108).

CONFIRMEDATARI2600-F133

Bank tracking verified live (1512 switches); the zero source narrows to the RAM display list

Asteroids f=800: bank=1, switches=1512 (~1.9 per frame — exactly the kernel-in-bank-0 / logic-in-bank-1 per-frame rhythm), last switch at $11FF0 (the bank-0 SwitchTo1 stub). The F132 cross-bank-data-read suspect is REFUTED: reforge_active_bank tracks faithfully (the lifted STA $FFF8/$FFF9 hotspot writes execute ~twice per frame), so bank-aware mem_read8 (F38) serves correct bytes to each bank's code.

BEING VERIFIEDATARI2600-F132

Asteroids black screen convicted: the game writes $00 to every color register; renderer innocent

The renderer and snapshot pipeline are innocent (write-then-clear sampling was also considered and excluded: the journal shows the written VALUES are zero). The L2 blocker is therefore the COLOR SOURCE CHAIN: the kernel stages colors from RAM/tables computed by overscan logic (the Pac-Man $D0-staging class), and that staging produces zeros — candidate causes: the color-computing routine not reached (F119 class), a color-table data read returning the wrong…

CONFIRMEDATARI2600-F130

Runtime-entry alias STITCHING implemented; the dispatch-miss feedback loop compounds again

Implementation simplification discovered during design: the F129 alias decode RE-SYNCHRONIZES with the natural alignment within a few instructions ($1159: ORA $85 consumes 2 bytes, $115B: ORA ($EA),Y consumes 2, landing on $115D which the natural pool already owns) — so full standalone alias functions are unnecessary. The fix is entry STITCHING: in engine.py's F8 multi-bank block (inside the console_id==atari2600 + Atari2600Mapper.is_banked gate —…

CORRECTED / SUPERSEDEDATARI2600-F127

Ratchet audit green; console_loop exit-code theory refuted (PS 5.1 stderr quirk was the chain-breaker)

G-A2600-004 iter 2, verification iteration. (1) REFUTATION: the working theory from the G2 ceremonies ("console_loop --run-prove-loop exits nonzero on Asteroids L0, breaking if-chained automation") is WRONG — console_loop.main() already returns 0 unconditionally after writing the report ("nonzero only for infrastructure failure", returns 2 only for missing corpus dir), and the empirical probe confirms EXITCODE=0 with $?=True.

CONFIRMEDATARI2600-F126

Tunnel pass-under adjudicated

G-A2600-004 opening iteration (corpus ratchet: document the drop, don't hide it). The v3 zero-SELECT TAS introduced a wall_collision FAIL ("sustained penetration", run of 9, first f=422).

BEING VERIFIEDATARI2600-F125

AC#3 verification trace consolidated; G-A2600-002 acceptance criteria all addressed

Docs-only consolidation closing G-A2600-002 AC#3. A "Collision Latch Verification Trace" subsection now sits directly under the TIA collision-detection spec (after the REFORGE implementation note), cross-referencing each spec contract (sticky-until-CXCLR, mid-scanline recompute-on-read, D6/D7-only bus) against the F118-F124 runtime evidence and recording the full latch->consumption->death->DEC chain with its permanent mechanical assertions.

BEING VERIFIEDATARI2600-F124

lives_decremented + ghost_collision_death asserted; the death pipeline is now a mechanical contract

G-A2600-002 AC#2 and AC#4 close-out. New evaluator evaluate_ghost_collision_death in oracles/mechanical_oracle.py: every [CXRD] consumed-collision event (the game READ CXPPMM bit7 set — the F118 read-hit telemetry) must be followed by a lives decrement in [PAC] within max_lag=300 frames (timing basis F119/F121: ~125f death freeze + up to ~130f $F500 DEC $E4 cadence before $F53E lands).

BEING VERIFIEDATARI2600-F122

Not invulnerability: two-player-alternating variation selected by the TAS

Edge-triggered [98W] log at the mem_write8 capture site (value-changing writes, frame floor 400 — the RESET-held boot window re-inits $98 every frame via the $FC86/$FBFE toggle and exhausted the first cap by f=41). (d) The [PAC] lvl=8 anomaly resolves: $99 is the GAME VARIATION register, not level — the TAS's 30-frame SELECT hold (f=280-310) advanced it to variation 8, a two-player alternating variation per the Atari game-variation table.

BEING VERIFIEDATARI2600-F120

$98 writer universe captured: the death-DEC at $F53E is NEVER reached

The [TRAP] $98 writer trap sat inside #ifdef REFORGE_VERBOSE since 09ag-3, so no default prove build ever printed it — the $F53E "death DEC" provenance (agent static disassembly) had never been runtime-checked. Moved the trap out of the VERBOSE gate (precedent: the minimal [MOV] trace).

BEING VERIFIEDATARI2600-F119

Death sequence runs end-to-end on a seen collision; lives ($98) are never decremented

Per-event collision context telemetry ([CXFIRE] one line per fire-frame at the latch-set site with screen position + game state; [CXRD] at the CXPPMM read path with state/$E7/$98/$99) plus a 2400-frame extended TAS run overturn the F118 read of the logic-side defect. A SECOND collision was seen at f=2368 (ghost walked into stationary pac) and the e7 cycle restarted — two complete death sequences in one run.

BEING VERIFIEDATARI2600-F116

Maze PF values come from zero-page staging, not a ROM display table

The F115 fork test (ROM byte comparison) returned a decisive negative with structure. (1) The journaled per-band PF sequences (49 band lines/frame) do NOT exist anywhere in ROM, forward or reversed — the maze playfield is COMPUTED, not read from a flat display table.

BEING VERIFIEDATARI2600-F114

Mid-scanline TIA write journal: capture step

Step 1 of the G-A2600-002 class-1 fix (built under G-A2600-001/AC#3 with the human-named design from verdict 4). Each TIASnap now carries the scanline write JOURNAL: up to 16 (cpu-cycle-offset, register, value) events for the visible-content registers (PF0/1/2, GRP0/1, COLUP0/1, COLUPF, COLUBK), captured by a single hook at the Atari io_write entry after TIA address normalization, staged per line, handed to the snapshot at the WSYNC strobe, cleared at…

CONFIRMEDATARI2600-F113

[SPR]-calibrated wall oracle catches the maze center passage missing from the render

Three results in one chain. (1) wall_collision calibration now comes from [SPR] telemetry instead of screenshot sprite-hunting: GRP1 drives the score/lives band EVERY frame (grp1_last pins at the band bottom), so window height cannot identify pac — but grp1_first can: it is pac top when drawn and the band fixed top when starved, making the band top the MODE of grp1_first.

BEING VERIFIEDATARI2600-F112

The sprite-vs-logic "drift" is a discrete one-band (22-row) misplacement toggle, not timing drift

First continuous [SPR] dataset (Finding 111) across 1800 TAS frames decomposes the F108 divergence completely. (1) When the pac sprite IS drawn (19-row GRP1 window), the RAM->render mapping is EXACTLY y_screen = 2*pacY + 6 (anchored rows) — locked for 150 straight frames at gameplay start and again in the f=1700+ wall-hold era; there is NO continuous drift and NO timing-residual component.

BEING VERIFIEDATARI2600-F111

[SPR] sprite row-window telemetry (GRP first/last scanline vs logic Y)

Instrumentation finding serving the F108v2 diagnosis (residual ~20-row era-dependent sprite-vs-logic vertical drift, sprite channel only). tia_publish_frame_truth() now tracks, per frame, the first/last visible scanline where GRP0 and GRP1 were nonzero plus the frame first-visible scanline (the F104 anchor), exposed via tia_grp{0,1}_{first,last}_sl() / tia_first_visible_sl().

BEING VERIFIEDATARI2600-F110

Cycle table v2: NMOS page-cross +1 cycles emitted (reads + taken branches)

Closes the two deferred items from Finding 103. (a) READ-class indexed ops (loads/ALU with abs,X / abs,Y / (ind),Y) now emit a runtime page-cross check alongside the base io_tick: abs-indexed compares the static base page against base+index (elided entirely when the base low byte is $00 — can never cross); (ind),Y fetches the zero-page pointer (with the NMOS (zp+1)&$FF pointer-high wrap) and ticks +1 when pointer+Y crosses.

BEING VERIFIEDATARI2600-F108

Sprite-vs-logic divergence measured

Built the maze-wall-map oracle for G-A2600-001 AC#3 (oracles/maze_map.py + phase2/console_loop.py wall_collision wiring): wall mask from the rendered playfield (large tan components inside the 189-row maze band; dots fall under a 31px size floor), sprite-shaped component detection, scale-aware RAM->pixel calibration with candidate voting, sustained-penetration run semantics (>= 4 consecutive in-wall samples; single-sample jitter is render quantization,…

BEING VERIFIEDATARI2600-F107

Per-frame [MOV] telemetry + wall_hold mechanical assertion (G-A2600-001 AC#2 mechanized)

Human verdict 4 confirmed walls hold visually; this finding converts that observation into a deterministic, regression-guarded Layer-3/4 assertion. (a) [MOV] moved from the 60-frame DBG cadence into the per-frame [PAC] telemetry block — at the old cadence a 30-frame wall-hold window contained at most one sample, indistinguishable from dead telemetry.

BEING VERIFIEDATARI2600-F104

Per-frame first-visible render anchor kills the screen shake

The frame composer COMPACTED visible scanlines (stacked top-down, skipping blanked lines): any mid-frame line drop made everything below it breathe upward, and frame-start variance translated content. v2 policy: each frame's first visible scanline pins to row 0 and all other lines map by ABSOLUTE offset from it — no interior compaction, no translation; gaps render black; bottom score/lives bands stay locked.

BEING VERIFIEDATARI2600-F103

Exact NMOS 6502 cycle table replaces per-IR estimates

io_tick costs came from flat per-IR-op guesses (loads 4, reg-ops 2, max-per-address): INC zp ticked 4 vs the real 5, PHA 4 vs 3, (ind),Y loads 4 vs 5 — thousands of small errors per frame. On a machine where 76 cycles = one scanline and 1 cycle = 3 beam pixels, that error budget IS the human-reported symptom cluster: RESP-strobed sprites land pixels away from where game logic believes them (sprite drift + phantom CXPPMM collisions = random deaths), and…

BEING VERIFIEDATARI2600-F100

Entity map correction: Pac-Man is ($B1,$B6); $B0/$BA is a ghost

The long-standing map (pacX=$B0/pacY=$BA) was watching ghost slot 0 — the same diagnostic-mislabel class as 09ag-16. GRP0 = ghost channel (4-frame color rotation), GRP1 = Pac (constant tan) — also inverted vs prior assumption.

BEING VERIFIEDATARI2600-F99b

Faithful JSR/RTS stack semantics; the ghost cluster-trap was lost abort semantics

JSR was a bare C call (no return-address push) and RTS a bare C return. Two corpus-wide consequences: (a) the universal "first-valid-wins" abort idiom (try-routine stores result, PLA/PLA discards the return address, RTS returns TWO levels) didn't abort — every direction try ran and the LAST valid one won.

BEING VERIFIEDATARI2600-F99

PHP/PLP were flag no-ops; Pac-Man's maze-row parity died in transit

The lifter pushed/pulled a dead r_flags vreg; the materialized model (ucmp carry, cmp_result N/Z, decimal_mode, overflow_flag) was never packed/unpacked. Pac-Man computes its legal-move table index as (X>>2) + 0x28*(row&1), preserving row parity in CARRY across two LSRs via PHP/PLP ($FBC5-$FBCE).

BEING VERIFIEDATARI2600-F98

input_c demo_mode: second phantom-switch source with a RESET deadlock

The interactive build had its OWN autoplay (09ag-23): RESET held f=10-270, fire/joystick pump after, latched off on first keypress — but a keypress DURING the hold window latched it off with key_reset still 1, pinning GAME RESET low forever (game locked in init, no select screen, no control). Removed entirely; window title now carries the key legend (Arrows/Z/Space/Enter=RESET/Tab=SELECT/F12=Power).

BEING VERIFIEDATARI2600-F97

Synthetic RESET/SELECT injection ran in interactive mode

The io_dispatch autoplay RESET hold (f=10-270) + SELECT probe (f=280-310) were not gated on headless_mode: a human player's first 4.5 seconds fought a phantom held RESET, then a phantom SELECT silently changed the game variation. Human play verdict ("no main menu") traced partly here.

BEING VERIFIEDATARI2600-F94

The lifter dropped decimal mode: SED/CLD were NOPs, all BCD score math ran binary

lifter.py lifted SED/CLD into the generic flag-NOP bucket, so the standard 2600 score idiom (SED / CLC / ADC score / CLD — Pac-Man's add-score routine at $FC45, and the same shape in most of the era's library) silently performed BINARY arithmetic: ten dots showed $0A instead of BCD $10, corrupting every displayed digit past 9. Fix: cpu->decimal_mode flag in CpuState; SED/CLD NOP-comments now emit set/clear; ADC/SBC emission branches at runtime into…

CONFIRMEDATARI2600-F93

TAS replay runtime + universal input-coupling oracle: the L3 gate is live

Layer 3 ("playable") was unverifiable because nothing could drive deterministic input: the .tas.json schema existed but the runtime had no replay support and --check-input-coupling was a stub. Landed end-to-end this session: (a) platform.c (via emitter_c.py) parses --tas-replay <path> and loads a flat event script ("<frame> <swcha-hex> <swchb_clear-hex> <fire 0|1>"; the C loader is a trivial fscanf — prove_loop converts .tas.json to this format so no…

CORRECTED / SUPERSEDEDATARI2600-F81

RESET-released-path hypothesis falsified by binary test

Concern: iter 47 hypothesised the RESET-released scan path at $107D produces the 271-frame exit. iter 48 implements the binary test: extend autoplay GAME RESET hold from frames 10-270 to 10-1500.

BEING VERIFIEDATARI2600-F78

dispatch-trace frame correlation

Concern: iter 44 left an open question: does autoplay's GAME RESET press window (frames 10-270) actually cover the frame when sub_001000 first executes? Without per-dispatch frame data, the question is unanswerable.

BEING VERIFIEDATARI2600-F72

Runtime dispatch trace reveals only 3 calls in 194 frames

Concern: Iter 38 closed with the bank-switch infrastructure landed in the static lifter, but the Asteroids runtime visual + frame count UNCHANGED. The static lift now contains the visible-frame kernel ($11003), dispatch writers ($1101B / $1109A), and at least one dispatch destination ($11083).

BEING VERIFIEDATARI2600-F71

Bank-switch hotspot byte-pattern scan

Concern: Iter 37's queue named bank-switch hotspot modelling as the principled fix for the runtime gap. Iter 33 + iter 34 + iter 35 + iter 37 progressively decoded more of Asteroids' bank-0 kernel code statically, but the runtime call chain still dispatches via sub_01143C (one of iter 24's const-prop seeds) and never reaches the visible-frame kernel.

BEING VERIFIEDATARI2600-F65

RIOT timer trace diagnostic; rules out §10.2 #1 as attract-mode wedge

Concern: Iter 31's audit picked dossier §10.2 #1 ("Timer never fires → TIM64T not modeled or INTIM not decrementing → attract state machine wedged") as the highest-prior diagnostic. Asteroids' attract mode renders only the score/lives HUD; the dossier explicitly names timer wedge as the canonical cause.

BEING VERIFIEDATARI2600-F63

G-A2600-005 corpus regression audit + acceptance assessment

Concern: With iter 29's chassis fix in place (full-pipeline regen on every prove), validate that G-A2600-005's F8 bank-switching work hasn't broken any non-F8 ROMs in the Atari corpus. Acceptance #4 ("fix applies to any F8-mapped cartridge") needs to hold against the actual lifted output, not the previously-cached output.

BEING VERIFIEDATARI2600-F62

Chassis: `prove_loop` runs full `RecompilationEngine` before template regen

Concern: Iter 28 (Finding 61) uncovered that prove_loop.regenerate() refreshes only templated files (io.c, ppu.c, apu.c, input.c, memory.{c,h}, debug.{h,c}, platform.c) — NOT game_main.c, which requires the full IR/CFG/lifter/emitter pipeline. This silently invalidated every "L2 GREEN preserved" verdict in iters 7-27 that touched disassembler/CFG/lifter/emitter source.

BEING VERIFIEDATARI2600-F60

Gap-bridge for unclassified data sandwiched between classified regions

Concern: Iter 26's threshold-24 filter caught the 30-byte POINTER_TABLE at ROM 0x09EA-0x0A08 right before Asteroids' fake-JMP region, but the walker still walked into bytes 0x0A08-0x0A11 (the actual sprite digit-shape data). Those bytes are unclassified by the analyzer — they don't form a valid pointer table after iter 24's cart-bit check, they're not padding, not printable text, not detectable as graphics on Atari 2600.

BEING VERIFIEDATARI2600-F59

Fall-through data threshold lowered 32 → 24

Concern: Iter 25's [DATA-AS-CODE] diagnostic on Asteroids still emitted 5 spans of small data overlap (8-30 insns each). The 32-byte minimum-region threshold for _is_high_confidence_data was conservatively chosen against the 1900 → 21 collapse risk, but the resulting cutoff missed a few legitimate data tables (notably a 30-byte POINTER_TABLE at ROM 0x09EA-0x0A08, just below the bar).

BEING VERIFIEDATARI2600-F58

Fall-through-only data filter in recursive descent

Concern: Iter 24's tightened pointer-table heuristic gave the data analyzer enough accuracy that the [DATA-AS-CODE] diagnostic could pinpoint actual fall-through-into-data paths (e.g. 0x119EA-0x11A06, 15 insns, ending 9 bytes before the fake-JMP at $1A0F).

BEING VERIFIEDATARI2600-F57

Atari 2600 pointer-table heuristic cart-bit validity

Concern: Iter 23's [DATA-AS-CODE] diagnostic on Asteroids reported 5 spans, the largest being 0x11000-0x11A06 (602 insns) covering essentially the entire walked body of sub_011000. The 602-insn span was so wide it provided poor signal — the diagnostic was dominated by the data analyzer's overall false-positive rate (~98.8% of ROM classified as data on Atari 2600).

BEING VERIFIEDATARI2600-F56

Data-as-code overlap diagnostic

Concern: Iter 22's [OUTER-LOOP-NATURAL-EXIT] confirmed that sub_011000 returned after reading uninitialised ($0038, $0039) to compute its trampoline target. Investigation revealed:

BEING VERIFIEDATARI2600-F46

Post-Scan Instruction Overlap Resolver — Misalignment Cascade Suppressed

Concern: Iter 12's interior-address filter caught misaligned const-prop seeds at seed time, but seeds in unvisited regions still passed through and their fall-through walks created decode cascades. Bank-0 virtual space had 189 overlapping instruction pairs (~30% of decoded code).

BEING VERIFIEDATARI2600-F45

Const-Prop Interior-Address Filter — Partial Misalignment Suppression

Concern: Iter 11 surfaced a misalignment cascade in bank-0 virtual address space: bank-0's sub_011B00 decoded a stray 0x02 JAM at $11B13 (mid-instruction of the STA $02 at bank-0 $1B12), and platform_exit(cpu) for that JAM is now Asteroids' visible exit point. Iter 12 attacks the cascade at its source: the multi-path const-prop's (lo × hi) cross-product over-seeds — for Asteroids' bank-0 walk, the const-prop discovers candidate targets like…

BEING VERIFIEDATARI2600-F43

JMP-Indirect Uses Outer Dispatch Loop (Not Nested C Call)

Concern: Iter 6 implemented JMP-indirect via a synchronous reforge_dispatch_indirect_jmp(cpu, target) helper that CALLED the matched function and returned. That's wrong semantics — real 6502 hardware NEVER returns from a JMP $XXXX: control flow transfers permanently.

BEING VERIFIEDATARI2600-F42

atari2600 Entry-Point Selection — Bank-1 Reset Vector, Not Lowest-Address Function

Concern: Iter 8's multi-path const-prop over-seeded function roots — the cross-product of immediate writers to ($F7, $F8) included combinations that don't correspond to real subroutine entries. Most were harmless extras (they decode some bytes, the lifter wraps them in a function, no other code references them).

BEING VERIFIEDATARI2600-F41

Multi-Path ZP-Indirect JMP Const-Propagation

Concern: Iter 7's single-path const-prop walked backward from one JMP site in address order and took the first matching LDA #imm / STA $ZP pair. Iter 8 generalises: enumerate EVERY immediate LDA writer for $ZP and $ZP+1, cross-product the (lo, hi) sets, seed all targets that map to ROM addresses.

BEING VERIFIEDATARI2600-F40

ZP-Indirect JMP Const-Propagation — Asteroids Discovers 6 More Functions

Concern: Iter 6 surfaced the runtime indirect-JMP target (0xD43C → bank-0 $143C) but no function was registered at $143C, so the dispatch helper exited cleanly with the target value in the log. The static lifter never decoded $143C because the recursive descent scanner only reached code via direct control flow + the explicit vector entries; bank-1's JMP ($00F7) is unresolvable through static analysis as-implemented (the existing _resolve_6502_indirect

BEING VERIFIEDATARI2600-F39

JMP-Indirect-via-ZP Runtime Dispatch — Asteroids Sees the Target

Concern: Iter 3 + iter 5 built the F8 bank-switch write/read sides. The third runtime link — control flow through JMP ($00F7) at $FFF3 — was a static-lift dead-end: the lifter emitted / jump to unresolved target bb_0000F7 / return; for any indirect JMP, because capstone's _parse_branch_target regex extracts $F7 from ($F7) and the lifter encodes it as label bb_0000F7 — which is never a valid label (zero-page is RAM, not code).

BEING VERIFIEDATARI2600-F37

F8 Bank 0 Static Disassembly — Asteroids Walks Both Banks

Concern: Iter 1+2 made the disassembler F8-aware but only ever walked the BOOT bank (bank 1) — because Atari2600Mapper.cpu_to_rom returned bank-1 bytes for any cart-window address, including bank 0's reset vector at $1591. Bank 0's instructions were never decoded, so the IR/CFG/C emitter had no visibility into post-switch code.

BEING VERIFIEDATARI2600-F36

F8 Runtime Bank-Switch State + Hotspot Handler in Emitter

Concern: Iter 1+2 made the static analyzer + disassembler F8-aware. Iter 3 lands the RUNTIME side: a reforge_active_bank global state that gets updated when generated code writes to the F8 bank-switch hotspots $FFF8 (select bank 0) or $FFF9 (select bank 1).

BEING VERIFIEDATARI2600-F35

F8 Cartridge Disassembler Mapper — Asteroids Walks Actual Bank-1 Code

Concern: Iter 1 (Finding 34) made the vector tracer F8-aware but the Atari2600Mapper in disassembler/recursive_descent.py:99-127 still treated the ROM as a single 4 KB blob. Walking from bank 1's reset vector $19DA read bank 0's data bytes at file offset $9DA (because addr - 0x1000 = $9DA always lands in bank 0).

BEING VERIFIEDATARI2600-F34

Asteroids F8 Bank-Switching — Diagnosis + Vector Tracer Becomes F8-Aware

Game: Asteroids (NA), 8 KB ROM Goal: G-A2600-005 (Asteroids F8 bank switching) Concern: Diagnose why Asteroids stays at L0 (build succeeds; runtime returns from game_entry at frame 0 with last_addr=0x1A9C); establish the F8 cartridge-format signal in the analyzer so downstream lifter / engine work has a concrete trigger.

BEING VERIFIEDATARI2600-F33

Layer 3 TAS Skeleton Bootstrap

Concern: G-A2600-001's acceptance criterion #2 ("TAS-driven 30-frame wall-hold: pacX delta within 1 pixel of expected") needs a tests/tas/atari2600/Pac_Man.tas.json file conforming to the Layer 3 schema documented in [internal specification]. Until the file exists, /prove's Layer 3 path falls back to the universal coupling smoke (drive joystick 30 frames each direction, assert ≥1 position delta) — enough for L3 but not L4.

BEING VERIFIEDATARI2600-F31

Layer 4 Re-Fire on FRESH Data

Game: Pac-Man (NA) Goal: G-A2600-001 (Pac-Man wall collision correctness) Concern: Iter 3's Layer 4 agent verdict (broken@0.95) was poisoned by stale PPMs (Finding 30). Iter 7 re-fires the agent oracle on the FRESH screenshots from the current build, and captures the L2 golden manifest now that visual_oracle._default_run_dir() resolves to the correct fresh directory.

BEING VERIFIEDATARI2600-F28

Pac-Man Palette Transition Bracketed to f=1400..f=1500

Game: Pac-Man (NA) Goal: G-A2600-001 (Pac-Man wall collision correctness) Status: ACTIVE (overriding prior iter 3 BLOCKED: agent-divergence per autonomous-mode policy — agent divergence is signal to investigate, not stop) Concern: narrow the f=800..f=1500 palette-transition window (Finding 27) so iter 5 has a 10-frame target instead of a 700-frame fishing expedition.

BEING VERIFIEDATARI2600-F27

Layer 4 Agent Oracle — Palette Shift Between f=800 and f=1500

Game: Pac-Man (NA) Goal: G-A2600-001 (Pac-Man wall collision correctness) Concern: Iter 3 fired the Layer 4 sub-agent oracle (/vision-style) on the four standard PPM screenshots (f=50/400/800/1500) to independently verify Finding 26's claim that Pac-Man is at the top maze row when pacY=$00. The agent classified the build as broken (0.95 confidence) and flagged a palette-shift artifact that Finding 26 did not catch.

BEING VERIFIEDATARI2600-F26

wall_collision Oracle False Positive — pacY=$00 Is The In-Maze Top Row

Game: Pac-Man (NA) Goal: G-A2600-001 (Pac-Man wall collision correctness) Concern: the wall_collision oracle in phase2/console_loop.py was failing on every 1800-frame Pac-Man run with pacY=$00 = off-maze top boundary. Iter 1 (Finding 25) added Y-write telemetry on the assumption that the writer at $FA23 was bypassing a missing maze-top guard.

BEING VERIFIEDATARI2600-F16

Writer-PC Stomp Bug Invalidated 6 Sessions of $82 Attribution

Game: Pac-Man (NA) Correction: Finding 15 said the $5F→$6E progression came from "$F857 (pacman_state_writer)." That label has been wrong since review cycle introduced the writer-PC capture infrastructure. $F857 is AND $0282 — Pac-Man's per-frame SWCHB COLOR/B&W switch read.

CONFIRMEDATARI2600-F14

Four TIA Primitive Audits — All Pass

Game: Pac-Man (NA) Trigger: Third-party review flagged four TIA contracts as suspect: RESPx/HMOVE timing, VDELP cross-latch, collision latch/clear, and SWCHB/$82 gating. After 09af landed unified TIA observation + generator hash gating + writer-PC symbolization, every audit was re-run against falsifiable data from the unified observation layer.

BEING VERIFIEDATARI2600-F13

$F882 INPT4 Read via TIA Mirror $3C

This is NOT a collision check — it's checking if the fire button is pressed during gameplay. The BPL at $F888 branches when fire is pressed (INPT4 bit 7 = 0), triggering game state reset.

BEING VERIFIEDATARI2600-F12

Mid-Scanline Collision Timing Critical for Death Detection

Game: Pac-Man (NA) Symptom: Ghosts never kill Pac-Man, can't eat frightened ghosts, even though CXPPMM=$80 at end of frame. Root Cause: Pac-Man reads CXPPMM at $F5D9 during the display kernel, right after writing GRP0 for the current ghost.

BEING VERIFIEDATARI2600-F9

MSVC /O2 Miscompiles io.c

Game: Pac-Man (NA) Bug: SIGSEGV crash at PC=$FBD5 in sub_00FBAD (movement calculation routine) — only in Release builds. Root Cause: MSVC /O2 optimizer miscompiles the complex switch/case logic in io.c with volatile pointer accesses.

BEING VERIFIEDATARI2600-F8

TIA Read/Write Register Overlap — INPT4 Clobbered by REFP1

Game: Pac-Man (NA) Bug: Game state ($82) stuck at 0x00 (attract mode) — never entering play mode. Root Cause: TIA read register INPT4 ($0C, fire button) and TIA write register REFP1 ($0C, player 1 reflect) share address $0C.

CONFIRMEDATARI2600-F6

Input Mapping Verified Working

Game: Pac-Man (NA) Confirmation: SWCHA correctly reads joystick state (saw SWCHA=$BF during test = LEFT pressed). Input handler maps: Arrow keys→SWCHA, Z→INPT4 fire, Enter→SWCHB RESET, RShift→SWCHB SELECT.

BEING VERIFIEDATARI2600-F3

RMW Write-Backs to $00 Cause Spurious VSYNC

Game: Pac-Man (NA) Symptom: Graphics "jumping and streaking" — display constantly resetting mid-frame. Root cause: Pac-Man uses ROR $00 (Read-Modify-Write on collision register/VSYNC address) for RNG.

CONFIRMEDATARI2600-F7

Audio System Works

Game: Pac-Man (NA) Confirmation: Manual playback confirmed that sounds and music were audible. Audio registers (AUDC0/1, AUDF0/1, AUDV0/1) at TIA addresses $15-$1A are correctly written through the default io_write path.

BEING VERIFIEDATARI2600-F4

LFSR Collision Simulation for Multi-ROR RNG

Game: Pac-Man (NA) Symptom: Game state never transitioned (stuck in attract mode) when collision registers returned static values. Root cause: Pac-Man uses two consecutive ROR $00 instructions to extract 2 carry bits from the collision register.

BEING VERIFIEDATARI2600-F2

6502 Flag Instructions Must NOT Modify Carry

Game: Pac-Man (NA) Symptom: No player movement, no AI, no game state transitions. Game logic branches based on carry flag were always taking the same path.

BEING VERIFIEDATARI2600-F1

TIA Write/Read Address Overlap Destroys State

Game: Pac-Man (NA) Symptom: CXCLR ($2C write) was zeroing TIA write registers (NUSIZ0/1, COLUP0/1) stored at cpu->memory[$00-$07], because collision registers (CXM0P-CXPPMM, read at $00-$07) shared the same memory locations. Root cause: On real TIA silicon, the READ address space ($00-$0D) and WRITE address space ($00-$2C) are physically separate circuits that share the same pin addresses.

BEING VERIFIEDATARI2600-F91

Corpus ROM-name matcher missed space-named dumps; three ROMs ran 7-week-stale lifts

prove_loop.find_rom tried underscore/hyphen variants only, so corpus dirs Space_Invaders / Missile_Command / River_Raid never matched "Space Invaders (NA).a26" / "Missile Command (NA).a26" / "River Raid (NA).a26" — full regen silently fell back to template-only, and those three ran game_main.c lifts dated April 23 (predating ~50 iterations of lifter fixes) while every prove run reported success.

BEING VERIFIEDATARI2600-F90

Visual-evidence pipeline was broken corpus-wide: screenshots written to a shared CWD and read stale

prove_loop.run_headless launched the exe without cwd, so the generated runtime's frame_NNNN.ppm writes landed in whatever directory prove_loop was launched from — every corpus ROM overwrote the same repo-root set, and the per-ROM build/Release/ dirs that console_loop globs for visual evidence never received fresh screenshots. The classifier then silently read months-stale files (Pac-Man's "screenshot evidence" dated April 9; Asteroids' May 23)…

BEING VERIFIEDATARI2600-F89

BRK is a walk terminator on the 6507 (padding fall-through blocks real routine entries)

The 6507 has no IRQ/NMI wired; games never execute BRK, so every $00 byte the walker meets is data or inter-routine padding. Falling through BRK chained the walk INTO padding runs, and a 2-byte BRK decode ending mid-padding spanned the first byte of the REAL routine after the padding — the overlap resolver then dropped the (unprotected) seeded decode as misaligned.

BEING VERIFIEDATARI2600-F88

Analyzer game-specific checks must be gated per title (the Pac-Man RAM-map phantom)

The [MOV] trace prints $B0/$BA/$BB (Pac-Man's entity map) on EVERY Atari ROM, and the analyzer escalated "1 unique position" to FAIL for all titles. Frogger — visibly playing in screenshots (river, logs, cars, frog) — carried a phantom "Player position FROZEN" FAIL for weeks; same for Space Invaders, Missile Command, Breakout.

BEING VERIFIEDATARI2600-F87

F8 cross-bank const-prop: bank-blind interior veto + unexported overlay targets (Asteroids DISPATCH-MISS 0x10C0)

Two compounding lift bugs kept Asteroids' kernel dispatch destinations unrooted. (A) _detect_zp_indirect_jmp_constprop Phase 3 built its misalignment veto from the MERGED instruction dict during the cross-bank overlay — bank-1 alias instructions at the same 13-bit window addresses vetoed clean bank-0 targets ($D0C0 masked 0x10C0 was "interior" of a bank-1 byte span; runtime exited at f=194 with DISPATCH-MISS target=0x10C0).

BEING VERIFIEDATARI2600-F86

Cycle-driven TIA beam model: scanlines advance every 76 CPU cycles, not only at WSYNC

The snapshot pipeline captured TIA state only in the WSYNC handler. Kernels that race the beam without WSYNC (Space Invaders' 6-invader rows: mid-scanline GRP0/GRP1 rewrites with PHA/PLA cycle padding, 2 scanlines per iteration, zero WSYNCs; every title's INTIM-paced VBLANK/overscan) starved the snapshot ring (SI: 77 snapshots vs 262 real lines) and the screenshot path extrapolated the last captured snapshot down the screen — SI's lower 2/3 was a solid…