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 011. 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,$01FFin 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.cxxinitializes 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 yieldsS=$FDafter 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
ENDMThe 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)
ENDMCold 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 022. Frame-Loop Anatomy ("Kernel Pattern")
2.1 NTSC frame structure
| Phase | Scanlines | Cycles | Notes |
|---|---|---|---|
| VSYNC | 3 | 228 | VSYNC bit 1 set, 3× WSYNC |
| VBLANK | 37 | 2,812 | VBLANK bit 1 set; game-state logic |
| Visible | 192 | 14,592 | Kernel — TIA writes track beam |
| Overscan | 30 | 2,280 | VBLANK bit 1 set; audio update, etc. |
| Total | 262 | 19,912 | 1 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
rtsThree things bite static recompilation here
- 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.
- 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. - **
STA RESP0,Xis 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 033. 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), pollINTIM($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
| Bit | Switch | 0 means | 1 means |
|---|---|---|---|
| D7 | P1 Difficulty | Amateur (B) | Pro (A) |
| D6 | P0 Difficulty | Amateur (B) | Pro (A) |
| D5 | (unused) | ||
| D4 | (unused) | ||
| D3 | TV Type | B/W | Color |
| D2 | (unused) | ||
| D1 | GAME SELECT | pressed | not pressed |
| D0 | GAME RESET | pressed | not 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 044. 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/$FFFDis read through the$F000mirror, which is why nearly every commercial ROM places its reset code withORG $F000even 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) | Mnemonic | Operation | Flags |
|---|---|---|---|
A7 B7 AF BF A3 B3 | LAX | A,X = M | N,Z |
87 97 8F 83 | SAX | M = A AND X (no flags affected) | — |
4B | ALR | A = (A AND imm) >> 1 | N,Z,C |
6B | ARR | A = ((A AND imm) >> 1) with C-rotate-in | N,Z,C,V |
07 17 0F 1F 1B 03 13 | SLO | ASL M; A = A OR M | N,Z,C |
47 57 4F 5F 5B 43 53 | SRE | LSR M; A = A XOR M | N,Z,C |
27 37 2F 3F 3B 23 33 | RLA | ROL M; A = A AND M | N,Z,C |
67 77 6F 7F 7B 63 73 | RRA | ROR M; A = A + M + C | N,Z,C,V |
C7 D7 CF DF DB C3 D3 | DCP | M = M − 1; CMP A,M | N,Z,C |
E7 F7 EF FF FB E3 F3 | ISC | M = M + 1; SBC A,M | N,Z,C,V |
9F 93 | AHX/SHA | M = A AND X AND (high(addr)+1) — UNSTABLE | — |
9E | SHX/SXA | M = X AND (high(addr)+1) — UNSTABLE | — |
9C | SHY/SYA | M = Y AND (high(addr)+1) — UNSTABLE | — |
0B 2B | ANC | A = A AND imm; C = N | N,Z,C |
EB | SBC* | Alias of legal $E9 SBC | N,Z,C,V |
02 12 22 32 42 52 62 72 92 B2 D2 F2 | JAM/KIL | Halt 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 055. 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).
| Read | Reg | Bit 7 | Bit 6 |
|---|---|---|---|
| $30 | CXM0P | M0/P1 | M0/P0 |
| $31 | CXM1P | M1/P0 | M1/P1 |
| $32 | CXP0FB | P0/PF | P0/BL |
| $33 | CXP1FB | P1/PF | P1/BL |
| $34 | CXM0FB | M0/PF | M0/BL |
| $35 | CXM1FB | M1/PF | M1/BL |
| $36 | CXBLPF | BL/PF | (no missile) |
| $37 | CXPPMM | P0/P1 | M0/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 066. 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
$0Ato TIM64T, INTIM reads$09next 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 077. 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.cxxinitializes 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 $1FF9patterns 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–$13FFvia hotspots$1FE0–$1FE7(banks 0–7)$1400–$17FFvia$1FE8–$1FEF$1800–$1BFFvia$1FF0–$1FF7$1C00–$1FFFfixed to bank 7 (last 1K of ROM — the boot/handler slot)
- Power-on: slot 3 is hardwired so the reset vector at
$FFFC/$FFFDcomes 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–$17FFselectable (ROM banks 0–6 via$1FE0–$1FE6, or 1K RAM via$1FE7);$1800–$19FF256-byte windowed RAM (4 banks via$1FE8–$1FEB);$1A00–$1FFFfixed 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–$17FFselectable 2K (any of up to 4 ROM banks; homebrew extends to 256);$1800–$1FFFfixed to last 2K. - Hotspot: write to
$003F— any value selects bank N. Overlaps TIA write range but$3Fis unused on TIA; reads still hit TIA mirrors. Bank-select is bySTA $3Fonly. - 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
$003Fmust 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 bankThe 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
| Mapper | Description / Detection | Marquee |
|---|---|---|
| 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 088. 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 $E3The 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:
| PC | Bytes | Effect | Next-dispatch target |
|---|---|---|---|
$D019 | A9 83 | LDA #$83 | $D083 |
$D01B | 85 E3 | STA $E3 | |
$D07A | A9 83 | LDA #$83 | $D083 |
$D07C | 85 E3 | STA $E3 | |
$D098 | A9 C0 | LDA #$C0 | $D0C0 |
$D09A | 85 E3 | STA $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 1Both 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
| PC | Insn | Target byte | Recovered target set |
|---|---|---|---|
$D00B | JMP (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 099. 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
CMPwhose 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 enterstbl_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-matchLDA #imm / PHA / LDA #imm / PHA / RTSand 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 ScreenSaverOffsetWhen 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:
- 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.
- 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.
- 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 class | Cycle-exact? | Tolerance |
|---|---|---|
| Sprite positioning (RESPx) | Exact | 0 cycles |
| HMOVE strobe + post-window | Exact | 0 cycles (must land in first ~3 cyc post-WSYNC) |
| WSYNC release | Exact (RDY-forced) | 0 cycles |
| Score kernel PF writes | Exact | 0 cycles |
| 2-line / 1-line kernel body | Exact | 0 cycles |
| VBLANK game logic | Loose | Must fit in 2,812 cyc |
| Overscan | Loose | Must fit in 2,280 cyc |
| Attract-mode state machine | Loose | TIM64T-driven |
| Collision processing | Loose | Read-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 1010. Recommended Runtime Checklist
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 |
CXM0P–CXPPMM ($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:
- Timer never fires → TIM64T not modeled or INTIM not decrementing → attract state machine wedged.
- SWCHA reads 0 → Pull-ups not modeled → every direction reads as "held"; demo or input filtering loops forever.
- INPT4/INPT5 read 0 (not $80) → Same pull-up issue → button reads as held; title screen skips or hangs.
- Frame counter doesn't increment → VSYNC frame-fence missing → RNG sequence identical every "frame."
- Collision latches don't clear →
CXCLRignored → death loop after first collision. - WSYNC doesn't stall → CPU free-runs through kernel → RESPx at wrong cycle → sprites at wrong column or off-screen.
- HMOVE comb not modeled → Visible artifact only; tier-3 priority.
- TIM64T pre-decrement not modeled → Game writes
$0A, reads$0Aback 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/PF2writes in the score-band kernel; assert digit values change after a staged input sequence. - Player sprite column change: Trace
RESP0strobe column over 30 frames with steady left-stick; assert monotonic leftward column. - Audio on collisions: Stage a collision; assert
AUDC0orAUDC1write 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/$1FF9accesses 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
ATARI2600-F182YARS' REVENGE ONBOARDED; MID-SCANLINE PLAYFIELD REPLAY RESTORES THE NEUTRAL ZONE
Yars' Revenge exposed the playfield half of Finding 181's deferred work. The first native build was live and interactive, but visual evidence alternated between a full-width chevron-like shield and an almost empty frame.
ATARI2600-F181CLASS 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").
ATARI2600-F180CLASS 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;…
ATARI2600-F179COMBAT 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.
ATARI2600-F178EVIDENCE-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.
ATARI2600-F177JSR PUSHED THE CANONICALIZED PAGE BYTE ($1A), NOT THE CPU'S LOGICAL 6502 PAGE ($FA)
MECHANISM: emitter_c.py _emit_call computed push_val = insn.source_address + 2 from the lifter's canonicalized/banked address. A JSR physically at $FA1D is canonicalized to $1A1D, so we pushed hi byte $1A where real hardware pushes the logical 16-bit page $FA.
ATARI2600-F176REFERENCE-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.
ATARI2600-F175TRAJECTORY 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…
ATARI2600-F174RIOT 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).
ATARI2600-F173TWO-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.
ATARI2600-F172THE SPIN LOOP DECODED: it is the asteroid SHAPE-STREAM draw loop, and the deadlock is a stream/scanline DESYNC
bank1[$0F09]=$88 ≠ bank0's $FF: the bank-aware read is FAITHFUL (bank 0 active, bank 0 byte served — F133 re-confirmed at the exact deadlock address).
ATARI2600-F171F170'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.…
ATARI2600-F170THE 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.
ATARI2600-F169F168 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).
ATARI2600-F168FREEZE 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…
ATARI2600-F167GOAL 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…
ATARI2600-F166VECTOR 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.
ATARI2600-F165F164 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.
ATARI2600-F164THE 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.
ATARI2600-F163prove_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)…
ATARI2600-F162TAS-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…
ATARI2600-F161THIRD 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.
ATARI2600-F160GAMEPLAY 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…
ATARI2600-F159F8 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.
ATARI2600-F158DYING 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--;…
ATARI2600-F157CORRECTION: the dispatch-miss feedback loop ALREADY EXISTS (engine.py, Findings 51/52) and it WORKED
One prove cycle after F156, the regen rooted $145 in BOTH banks: case 0x1145: if (reforge_active_bank == 0) sub_011145(cpu); else sub_001145(cpu);. The new run produces NO DISPATCH-MISS — the kernel dispatch happens — but STILL exits at f=313 with [OUTER-LOOP-NATURAL-EXIT] last_dispatched=0x1145.
ATARI2600-F156The 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…
ATARI2600-F155Analyzer 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.
ATARI2600-F154THE 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.
ATARI2600-F153F152 REFUTED both ways: the terminator IS written on every build ($FE71
STATIC refutation of F152: both suspect callees are clean. Neither can fire the caller's unwind guard.
ATARI2600-F152ROOT-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;.
ATARI2600-F151TWO-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…
ATARI2600-F150Game-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…
ATARI2600-F149$BF lifecycle COMPLETE: cleared at $F110 gated on $80 bit5, consumed by the $F114 init gate, decremented at $11ADE
DECISIVE NEGATIVE: the iter-18 fixed watch shows ZERO $F110 writes and ZERO $F114 reads in 280 frames — the ENTIRE $F101+ region never executes; same for $11ADE. All of it lives inside sub_00100E (the main boot/frame flow, dispatcher-called) — so a branch INSIDE sub_00100E diverts before $F101 every frame.
ATARI2600-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.
ATARI2600-F147Asteroids 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…
ATARI2600-F146Object 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.
ATARI2600-F145Init 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).
ATARI2600-F144F143 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…
ATARI2600-F143TWO 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.
ATARI2600-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.
ATARI2600-F141Object 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.
ATARI2600-F140Sentinel 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.
ATARI2600-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.
ATARI2600-F138Asteroids 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.
ATARI2600-F137Kernel 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.
ATARI2600-F136Flicker 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).
ATARI2600-F135Widened 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).
ATARI2600-F134Journal 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).
ATARI2600-F133Bank 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.
ATARI2600-F132Asteroids 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…
ATARI2600-F131ASTEROIDS REACHES L1: dispatch-miss chain exhausted in two cycles; render gap (black frames) is the L2 blocker
Chain burn results: cycle 1 f=324 (miss 0x190F resolved, next 0x1834); cycle 2 f=1800, NO dispatch miss — the full 1800-frame headless run completes. After ~53 iterations across two goal-runs, Asteroids moves L0 -> L1.
ATARI2600-F130Runtime-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 —…
ATARI2600-F129$1159 is a deliberate mid-instruction kernel-ladder entry; the single-decode-per-address model cannot represent it
Forensic complete; the F128 suspicion chain resolves three layers deep. (1) BANK ATTRIBUTION: the exit site virtual 0x11108 is BANK-0 $1108 — the JMP ($E5) is the Asteroids display kernel's computed goto.
ATARI2600-F128Asteroids re-baseline: the $1159 dispatch-miss seed vanishes inside the lift pipeline
G-A2600-005 run 1 opening measurement (resuming the 49-heartbeat G5 arc at its iter-49/51 state). This is PROGRESS relative to the iter-51 memory ($E6=$00 -> $00CF garbage): the vector now resolves to a plausible bank-1 address; the blocker has moved from vector-corruption to function-coverage.
ATARI2600-F127Ratchet 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.
ATARI2600-F126Tunnel 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).
ATARI2600-F125AC#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.
ATARI2600-F124lives_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).
ATARI2600-F123One SELECT EDGE (not hold duration) selects the 2-player variation
Three results, one refinement of F122. (1) REFUTATION: trimming the SELECT hold 31->2 frames produced a BYTE-IDENTICAL run (same death frames, same $99=8) — and $99 is 8 from FRAME 1, before any input.
ATARI2600-F122Not 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.
ATARI2600-F121The death-DEC EXECUTES; lives are re-initialized to 3 during respawn (refines F120)
DEC $98 RAN. Yet every [PAC] frame sample read lives=3 and the poll-based [TRAP] $98 printed nothing: lives went 3->2->3 inside one poll gap.
ATARI2600-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).
ATARI2600-F119Death 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.
ATARI2600-F118Ghost-collision invulnerability split into two defects by cumulative CXPPMM telemetry
G-A2600-002 opening measurement. New always-on counters in io_dispatch_c.py (never reset by CXCLR, so they survive the game's per-frame latch strobe that made [CX] sampling blind): frames-with-P0/P1-latch-fire and CXPPMM reads that returned bit7 set, both surfaced on the per-frame [PAC] line.
ATARI2600-F117Dot-clear chain proven live; center bar adjudicated authentic; wall_collision PASSES
Closing the F113->F115->F116 investigation. (1) Cross-frame journal diff (f=400 vs f=1600, two-tag [JRNL] dump): band lines sl 190-191 changed PF0 $A0 -> $00 — an eaten dot's playfield cells CLEARED.
ATARI2600-F116Maze 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.
ATARI2600-F115Journal contents refute the mid-scanline-gap hypothesis; the game itself writes the center bar solid
Two results. (1) Journal plumbing correction: a scanline can be snapshotted more than once (WSYNC path AND the Finding-86 cycle-driven beam path); the capture handoff used assignment, so the later empty-stage call zeroed ev_n on every line while the capture counters grew — every snapshot read as event-free.
ATARI2600-F114Mid-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…
ATARI2600-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.
ATARI2600-F112The 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.
ATARI2600-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().
ATARI2600-F110Cycle 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.
ATARI2600-F109R-MECH-4 is a real gate: checksums now evaluates every existing mechanical oracle
Chassis finding (Finding 102 class). The blocking CHECKSUMS rule R-MECH-4 ("per-ROM mechanical assertions pass, L4") had been a stub returning pass since the revival migration — the L4 ladder rung existed on paper only.
ATARI2600-F108Sprite-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,…
ATARI2600-F107Per-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.
ATARI2600-F104Per-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.
ATARI2600-F103Exact 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…
ATARI2600-F102Template regen emitted default screen dims; interactive launch crashed after the 250-line window
prove_loop's template regenerate() constructed its CEmitter without screen dimensions, so every regenerated platform.c called ppu_init(256, 240) for every console. Latent for months: the Atari SDL render walk stayed within 240 rows, so the wrong-sized window merely letterboxed.
ATARI2600-F101TIA render: P0>P1 fixed priority + 250-line window restores the score/lives bands
(a) render_scanline_from_snap drew P1 after P0 (P1 won overlaps); real TIA fixed priority is P0>M0>P1>M1>BL>PF. Draw order swapped.
ATARI2600-F100Entity 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.
ATARI2600-F99bFaithful 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.
ATARI2600-F99PHP/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).
ATARI2600-F98input_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).
ATARI2600-F97Synthetic 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.
ATARI2600-F96Nested-layout builds compiled a 7-week-stale game_main.c: full-regen never copied the lift into the c_project
prove_loop.copy_to_project copied only runtime templates (io.c, ppu.c, ...) into nested c_project dirs. full_regen wrote the fresh game_main.c into the game_dir root — and the build compiled Pac_Man_NA_c_project/game_main.c, dated April 23.
ATARI2600-F95The 09-series RAM probes were corrupting the state they were meant to unlock
This record documents the conclusion: The 09-series RAM probes were corrupting the state they were meant to unlock. Supporting raw traces remain outside the public finding.
ATARI2600-F94The 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…
ATARI2600-F93TAS 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…
ATARI2600-F83Autoplay GAME RESET hold (iter 48 binary-test probe) left active for every corpus ROM
The iter-48 Asteroids binary test extended the synthetic GAME RESET hold from frames 10-270 to 10-1500 and was never reverted after the test concluded. Every corpus ROM therefore ran with the console's RESET switch held for 25 of the 30 simulated seconds.
ATARI2600-F82protected-seed override in `_drop_overlapping_instructions` unconditionally favours dispatch_miss seeds, even when…
Concern: iter 48 identified sub_001046 (misaligned function at bank-1 $1046) as the runtime exit cause. iter 49 audits why iter 41's misaligned-leader filter didn't catch it.
ATARI2600-F81RESET-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.
ATARI2600-F80$E3/$E4 init IS correct in RESET-pressed cycles; the real exit is at $104F in the RESET-released scan path
Concern: iter 46 attributed the 271-frame exit to "$E3/$E4 = 0 after ZP-clear at $106B-$106E". Iter 47 adds a $E3/$E4/$E5/$E6 write trace (windowed at frames 0-2 / 190-210 / 260-280) to confirm whether/when these pointers get set.
ATARI2600-F79bank-collision in dispatch switch picks wrong function for F8 carts
Concern: iter 45's diagnostic showed first $1000 dispatch at frame 195 within autoplay's RESET window, yet chain still ends $19DA. iter 46 traces $F7/$F8 writes by PC + frame to disambiguate.
ATARI2600-F78dispatch-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.
ATARI2600-F77Asteroids ROM has ZERO standard INPT4/INPT5 read patterns; iter 43 hypothesis falsified
Concern: iter 43 hypothesised that Asteroids' fire-button polling code exists in undecoded ROM regions. Iter 44 ROM-byte search exhaustively tests this.
ATARI2600-F76soft-reset cycle is the game's attract-mode flow, not a bug; fire-button polling absent from lifted code
Concern: iter 42 named the cycle $143C → $1000 → $19DA → $1B00 a "soft-reset trap". iter 43's task: audit sub_01143C exit paths to find the writer of trampoline_target = $1000.
ATARI2600-F75$1100` DISPATCH-MISS already resolved by iter-41 + dispatch_miss_cache feedback
Concern: Investigate the $1100 DISPATCH-MISS that iter 41's commit row reported as the runtime exit point. Either the function isn't decoded (no walker path led there) or it's decoded under the bank-0 virtual prefix $11100 and the dispatch-table name resolution is matching wrong.
ATARI2600-F74Drop misaligned CFG leaders; runtime dispatch chain extends from 3 → 5 calls
Concern: Iter 40's audit identified $114BA as a misaligned CFG leader splitting the kernel function. Iter 41 implements fix candidate (1): drop leaders whose address falls inside another decoded instruction's byte range.
ATARI2600-F73Misaligned CFG leader $114BA splits the kernel function; sub_01143C falls off end with no terminator
Concern: Iter 39 named iter-40 forensic concerns: inspect sub_01143C (the 194-frame dead-end function) for trampoline_target writes; grep all lifted code for trampoline_target = $1003. Both were specifically called out.
ATARI2600-F72Runtime 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).
ATARI2600-F71Bank-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.
ATARI2600-F70Cross-bank ZP-pointer const-prop resolves $E3/$E4 dispatch; $11083 decoded
Concern: Iter 35-36 left two dispatch destinations missing ($11083 / $110C0) and two of the three STA $E3 writers ($1107C, $1109A) missing. Iter 35's queue named two-byte ZP-pointer const-prop for JMP ($zp) as the highest-leverage fix.
ATARI2600-F69Bank-1 byte-pattern scan attempted, regressed Asteroids, reverted; helper extraction retained
Concern: Iter 35's byte-pattern scan (with opcode source seeding) was bank-0-only. Iter 36 candidate (2) from the queue: "Symmetric byte-pattern scan in bank-1 (currently bank-0 only).
ATARI2600-F68Byte-pattern scan extended to opcode source positions; JMP-$D003 sites + new runtime path
Concern: Iter 34's byte-pattern scan was target-only — it seeded the destination of every 4C XX YY (JMP abs) / 20 XX YY (JSR abs) opcode position, but not the source position itself. As a result, the four JMP $D003 source sites in Asteroids ($1125 / $113B / $11A9 / $1215) stayed missing: no decoded reference pointed at them.
ATARI2600-F67Bank-0 byte-pattern JMP/JSR target scan; kernel entry now decoded
The first fix candidate from iter 33's queue was a fixed-point bank-0 walker: collect bank-0's own branch_targets after each pass and re-seed.
ATARI2600-F66Bank-0 walker under-coverage: the kernel entry and dispatch writers are never decoded
Concern: Iter 32 ruled out the RIOT timer as the attract-mode wedge. The dossier's next-priority hypothesis was the JMP (mE3) target-resolution audit (§3.3 / §8.2): are the dispatch targets {$D083, $D0C0, …} reachable in our lifted code?
ATARI2600-F65RIOT 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.
ATARI2600-F64Deep-research dossier integrated; SWCHB pull-up default fix; visual ground truth captured
Concern: Iter 30's READY-FOR-REVIEW was based on the analyzer's "OK all checks passed" verdict for Asteroids. The user opened the .exe and observed the screen renders only the score+lives HUD on a black background — no title text, no rotating asteroids, no demo animation.
ATARI2600-F63G-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.
ATARI2600-F62Chassis: `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.
ATARI2600-F61Chassis: `prove_loop` does not regenerate `game_main.c`; iter 25-27 walker filter reverted
Concern: Iter 27 noted that with the gap-bridge filter live, the bank-0 walker decoded only 3 instructions in the $19E0-$1A20 range, yet game_main.c still contained the fake-JMP region $119EA-$11A0F. This was treated as "downstream emitter pipeline reconstructing filtered content" and deferred to iter 28+ investigation.
ATARI2600-F60Gap-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.
ATARI2600-F59Fall-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).
ATARI2600-F58Fall-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).
ATARI2600-F57Atari 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).
ATARI2600-F56Data-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:
ATARI2600-F55Outer-Loop Natural-Exit Diagnostic — sub_011000 Returns Without Setting Trampoline
The outer dispatch loop exited cleanly but for an unidentified reason. The loop's exit condition is cpu->running && cpu->trampoline_target — at least one of those was false.
ATARI2600-F54Scoped Seed Protection — Trace-Feedback Targets Override Fall-Through Overlap
Concern: Iter 20 discovered the trace-feedback loop's convergence ceiling: $10C0 kept recurring as a dispatch miss despite being in extra_entry_points. The CFG-builder couldn't make $10C0 a function root (no instruction at the leader address).
ATARI2600-F53Trace-Feedback Loop Convergence Run — 123→238 Functions, Plateau at 194 Frames
Concern: Iter 19's monotonic trace-feedback loop is operational. Iter 20 RUNS the loop to convergence within a single iter (no further source changes — just exercise the existing mechanism) and documents the convergence behavior + the next blocker.
ATARI2600-F52Trace-Feedback Loop Converges — Best-Effort Dispatch + Cache Accumulator
Concern: Iter 18's trace-feedback loop had two stability problems: 1. One target per run: the outer dispatch loop's default case set cpu->running = 0 on the FIRST miss, so each prove_loop run surfaced exactly one new target.
ATARI2600-F51Runtime Trace Feedback — sub_001A07 (1655 insns) Lifted from prove_log [DISPATCH-MISS]
Concern: Iter 17 restored the [DISPATCH-MISS] target=0xXXXX log line, surfacing concrete runtime-needed function addresses. The trace + feedback loop closes the static-vs-runtime gap iteratively.
ATARI2600-F50Dispatch-Miss Log Restores Runtime Visibility — Asteroids' Phase-2 Target Surfaced
Concern: Iter 16's chassis improvements landed but Asteroids' frame count stayed at 194 with last_addr=0x11FF3 (bank-0's JMP-indirect trampoline). The outer dispatch loop's default case set cpu->running = 0 silently — no log line.
ATARI2600-F49Bank-0 High-Mirror Canonicalisation — Asteroids Has Correct Bank-Switch Trampoline
Concern: Iter 15's breakthrough revealed a new blocker at $11BC3 — bank-0 lifted code that JMPs to $FFF0 (the bank-switch trampoline). The runtime sets cpu->trampoline_target = 0x1FF0 (the masked form per iter 14's emit), but no function at $1FF0 or $11FF0 exists in the dispatch table.
ATARI2600-F48Masked Branch-Target Seeds Promote $1B24 etc. to Function Roots — Asteroids RUNS 194 FRAMES
🎯 BREAKTHROUGH ITER. After 11 iters (4-14) chipping at F8 bank-switching infrastructure, iter 15 lands the missing piece: cross-bank-jump targets get their masked-and-bank-mirrored form added as CFG entry points without rewriting the source instructions' branch_targets.
ATARI2600-F47Direct-JMP Unresolved Routes Through Trampoline (Masked)
Concern: Iter 13's overlap resolver exposed an unresolved direct JMP at $11BAE (JMP $DB24, bytes 4C 24 DB). Capstone returns branch_target=$DB24 — the raw 16-bit operand.
ATARI2600-F46Post-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).
ATARI2600-F45Const-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…
ATARI2600-F44Bank-0 Walk Receives Const-Prop Targets — Asteroids Hits Bank-0 Code at Runtime
Concern: Iter 10 surfaced a critical bug. The lifter's sub_001B00 had BANK 1's bytes (FC 85 F4 A2 ...) at CPU $1B00, not bank-0's (A9 03 85 25 ...).
ATARI2600-F43JMP-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.
ATARI2600-F42atari2600 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).
ATARI2600-F41Multi-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.
ATARI2600-F40ZP-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…
ATARI2600-F39JMP-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).
ATARI2600-F38F8 Bank-Aware mem_read8 — Operand Fetches Honor Active Bank
Concern: Iter 3 landed reforge_active_bank + STA $1FF8/$1FF9 hotspot tracking. Iter 4 made the static disassembler walk bank 0.
ATARI2600-F37F8 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.
ATARI2600-F36F8 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).
ATARI2600-F35F8 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).
ATARI2600-F34Asteroids 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.
ATARI2600-F33Layer 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.
ATARI2600-F32L2 Golden Manifest Now Self-Describing — `captured_at` + `generator_hash_at_capture` Filled From Real Sources
Concern: Iter 7 captured the first L2 golden manifest but the captured_at and generator_hash_at_capture fields were placeholder TODOs (pre-existing oracle-infrastructure debt). R-REBASELINE-PROVENANCE-1 can't validate a rebaseline without real provenance metadata, and a manifest with "TODO-utc" defeats the point of the schema.
ATARI2600-F31Layer 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.
ATARI2600-F30Stale `screenshots/` Cache Caused Phantom Palette Bug (Findings 27-29 Premise Retracted)
Game: Pac-Man (NA) Goal: G-A2600-001 (Pac-Man wall collision correctness) Concern: Iter 6 added multi-scanline [PALETTE f%u sl%d] dump (sl=20/60/100/140/180) in the f=1400..1500 window. The data should have shown which scanlines transitioned.
ATARI2600-F29Mid-Screen Palette Sample sl=100 Is Stable Across the f=1400..f=1500 Transition Window
Game: Pac-Man (NA) Goal: G-A2600-001 (Pac-Man wall collision correctness) Concern: Iter 4 (Finding 28) bracketed the palette transition to f=1400..f=1500. Iter 5 instruments per-5-frame palette samples in that window to identify the exact transition frame.
ATARI2600-F28Pac-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.
ATARI2600-F27Layer 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.
ATARI2600-F26wall_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.
ATARI2600-F25Pac-Man Y-Write Old/New/Frame Telemetry — Maze-Boundary Bypass Happens 13 Frames Before Oracle Sample
Game: Pac-Man (NA) Goal: G-A2600-001 (Pac-Man wall collision correctness) Concern: add yOld/yNew/yFrame telemetry to [MOV] trace + wall_collision oracle failure detail so the Y-write that brings pacY to $00 is observable, not just the post-write sample.
ATARI2600-F24PHA/PHA/RTS Jump-Table Dispatch — Lifter Unblocks Missile_Command + River_Raid
Games affected: Missile Command (dispatch by game-mode table at $F597/$F598, 8 targets), River Raid (dispatch by game-stage table at $FBEF/$FDF6, 10 targets). Both previously terminated at frame 0 with game_entry returned (running=1, frame=0, last_addr=0x{F596,1139}).
ATARI2600-F23Analyzer False-FAIL Cleanup — 6 ROMs Reclassified GREEN
Games affected: Combat, Adventure, Frogger, Breakout, Pitfall, Space_Invaders (plus existing Pac-Man GREEN). Symptom: prove_loop exit codes misaligned with visual ground truth.
ATARI2600-F16Writer-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.
ATARI2600-F15$82 Progression Is the Attract-Demo Timer, Not Gameplay
Game: Pac-Man (NA) Correction: Finding 14 above framed the 5000f $82 progression to $6B as five new states beyond the 09aa best of $66. This was wrong.
ATARI2600-F14Four 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.
ATARI2600-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.
ATARI2600-F12Mid-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.
ATARI2600-F11VSYNC Guard Too Conservative — Pac-Man Writes VSYNC at sl=193
Game: Pac-Man (NA) Bug: Scanline count fluctuated: 259, 263, 265, 271, 288, 326, 511 per frame instead of ~193. Root Cause: VSYNC handler guard required scanline_count >= 200.
ATARI2600-F10Variable RESP0 Sprite Positioning Confirmed Working
Game: Pac-Man (NA) Investigation: Debug output showed P0=40, P1=51 constant. Initially suspected RESP0 always strobed at same cycle.
ATARI2600-F9MSVC /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.
ATARI2600-F8TIA 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.
ATARI2600-F6Input 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.
ATARI2600-F5Game State Oscillation — RAM[$82] Root Cause
Game: Pac-Man (NA) Symptom: RAM[$82] (game state: 0=attract, 1=playing) oscillates between 0 and 1 every few frames. Game never enters stable play mode.
ATARI2600-F3RMW 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.
ATARI2600-F7Audio 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.
ATARI2600-F4LFSR 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.
ATARI2600-F26502 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.
ATARI2600-F1TIA 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.
ATARI2600-F91Corpus 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.
ATARI2600-F90Visual-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)…
ATARI2600-F89BRK 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.
ATARI2600-F88Analyzer 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.
ATARI2600-F87F8 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).
ATARI2600-F86Cycle-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…
ATARI2600-F85Emitter dropped the entry-block goto when the function entry is not the lowest-address block (Missile Command + River…
emitter_c.py emit_function emitted blocks address-sorted and jumped to the entry label only when blocks[0] was a "stale RTS" stub — otherwise execution FELL INTO blocks[0]. A 6502 control transfer (JSR, PHA/PHA/RTS dispatch, indirect trampoline) always begins at the target address, so the fall-in is never correct.
ATARI2600-F84TIA VSYNC decodes only D1; the `val <= 0x03` guard rejected hardware-legal assertions (Breakout $FF, River Raid $82)
Real TIA VSYNC latches D1 only. Breakout strobes VSYNC via STX with X=$FF (triple-duty: NUSIZ=$FF quad-width paddle / 8-clock missiles + VSYNC D1), River Raid asserts with $82 (bit-7 dump rider).