MUSCLE (U): decode the bytecode VM layer (Pass 3b)

The 42 token programs were the last undecoded part of MUSCLE. Each now carries a
token-by-token listing above its unchanged .byte data, generated by
_tools/bytecode_dis.py and wired into the build as 'make pass3'.

Encoding, derived from RunBytecodeInterpreter ($9AE8) and BytecodeHandlerTable
($9237): the dispatch is one byte per element. A byte in $C8-$EF selects one of
the 40 handler-table entries; every other byte takes the literal path at $9B08
and is pushed onto the operand list at $0033[Y], accumulated into $003C[Y].

The one exception is four opcodes whose handler's first instructions are
'lda ($1A),Y / iny', so they consume the following byte inline:
  $CB LOOP_BEGIN  -> $7C,X count + $7E,X body address
  $D1 SET_RATE    -> $43,X
  $D7 DRAW_SPRITE -> count for the $A660 blit
  $E1 SET_WAIT    -> $7A,X
$CB and $C9 are the loop pair: LOOP_END decrements $7C,X and, if non-zero,
reloads Y from $7E,X, rewinding the interpreter to the top of the body.

Decoded 377 opcodes and 937 literal operands across 42 programs; all 40 defined
opcodes are used, so the opcode set is complete.

Two standalone programs contain no byte in $C8-$EF at all, so every byte takes
the literal path - the listing states that rather than implying code.

Each mnemonic was read off its handler's own straight-line path up to its first
'jmp $9AE8' / 'jmp $9B25'. Handlers that only tail-call a game routine are named
for the routine rather than guessed at, since the semantics live in the callee.

bytecode_dis.py inserts comment lines only; .byte data is copied through
verbatim. Verified: all 16,384 PRG bytes still emitted exactly once, every value
equal to the ROM, zero audit mismatches, and 'make regen' (pass1 -> pass2 ->
pass3) reproduces prg.s byte-for-byte. Byte-identical ROM verified.

MUSCLE (U): trace the stage payload with py65 (Pass 3c)

The stage payload at $B0FB-$BEBF could not be decoded statically, and the
trace confirms why: across all 16 KB of code there is exactly ONE operand in
that range - $BD28, the literal pointer table itself. The real address is
formed at runtime by 'adc $BD28,X', where A is a value carried in the
bytecode stream, so the readers are indexed by a runtime value and cannot be
enumerated by searching the code.

FCEUX cannot observe this either: its Lua API offers registerexec /
registerexecute / registerrun / registerwrite but no memory-read hook, and with
no static list of readers there is nothing to hook.

_tools/trace_stage_payload.py therefore uses py65's
ObservableMemory.subscribe_to_read, which fires on every read of a range and
can report PC from inside the hook. py65 is only a 6502 core, so the tracer
adds the minimum hardware to boot the game: a scanline counter off
processorCycles, $2002 vblank at scanline 241 cleared on read, NMI at the same
scanline, $4014 OAM DMA, and the $2005/$2006/$2007 latches. Without NMI the
game never leaves $80E0 (lda $0321 / beq), because $0321 is the flag the
bytecode VM raises.

Results over 300 frames (26.8M cycles, 4,071 NMIs): 2,548 payload reads from
44 distinct reader PCs, touching 914 of 3,421 bytes.

Findings:
- The hottest reader is $AC82, the compressed-VRAM RLE decoder: 1,046 of its
  1,128 reads advance by exactly +1, comparing each byte against $C0.
- $9B50 is a backward copy (lda ($12),Y / sta ($10),Y / dey), stepping -1.
- 107 distinct 8-byte blocks had all eight bytes read, with spacing dominated
  by $08 - a dense 8-byte record array - plus a sparser group on a $D0 stride
  ($BBE0, $BCB0, $BD80, $BE50).
- Readers use three disciplines: forward sequential, backward sequential, and
  variable stride consistent with a fixed-offset field walk.

py65 is deliberately NOT a build dependency: the tool documents unpacking the
wheel on demand. 'make regen' is unaffected. Byte-identical ROM verified.

MUSCLE (U): fix the NMI/controller bugs in the payload tracer; correct the Pass 3c model

The Pass 3c tracer had two bugs that were suppressing all progress, both now
fixed in _tools/trace_stage_payload.py (previously scratch, now durable):

* NMI re-armed by $2002 reads. The vblank flag must be cleared once per frame,
  not on every read of $2002 -- the main loop polls $2002 and spins on the
  bytecode VM flag at $8226, so each poll cleared bit 7 and re-armed NMI inside
  the same scanline. That gave 40,900 NMIs across 3,000 frames and corrupted all
  game state. A per-frame nmi_fired flag restores exactly one NMI per frame
  (verified: 600 frames -> 600 NMIs).

* Input went to $4017. The poll routine at $AA69 is `lda $4016,X` with X=1 and
  only compares bits 0-1, so driving $4017 consumed nothing -- all masks tested
  gave byte-identical traces. The game consumes joy1 on $4016 and needs a real
  latch/shift register. With it, START matters: 47.7% coverage vs 39.0%.
  Match entry is two discrete START presses (title -> char select -> match).

Coverage 26.3% -> 40.9%. Both bugs and the port subtlety are documented in the
tool header so they are not re-derived.

Corrects the Pass 3c conclusion. $B0FB-$BEBF is NOT per-stage level data:

* $BD28 is an 8-entry per-ACTOR entry-offset table, not a stage table.
  $A063 `adc $BD28,X` with X = ActorIndex ($1E), plus #$32 at $A071 when
  PlayerActiveFlag ($78,X) is set. Observed actors are $00-$04.
  Forcing all 8 actor indices adds zero new bytes.

* The sound engine reads the same window: $B03C `lda (SoundReadPointerLo),Y`
  with `cmp #$F0 / sbc #$F0 / tay / lda $B0E3,y` is note decoding, which is
  where the $FF/$F0 high-nibble terminators come from.

So the window is a shared pool -- per-actor bytecode, compressed-VRAM RLE, and
music note streams -- and stage cycling cannot raise coverage past ~40%. The
remaining 2,161 B need other music tracks and scripted match play, not taps.

Also corrects two stale claims: $0326 is not a level byte (never leaves $00 even
in a confirmed match), and $0320 is a boolean, not a screen-state machine.

Build unaffected: byte-identical, and `make regen` still reproduces prg.s
byte-for-byte.

MUSCLE (U): promote to the top-level inventory as #16

git mv _wip/MUSCLE (U) -> MUSCLE (U). The relative symlink
copilot/rom.nes -> ../M.U.S.C.L.E. (U).nes survives the move untouched.

* copilot/Makefile: tool paths ../../../_tools/ -> ../../_tools/ for NESDIS,
  PASS2 and BYTECODE. `make` prints the byte-identity message and `make regen`
  still reproduces prg.s byte-for-byte, both verified after the move.
* MUSCLE (U)/OVERVIEW.md: ROM/build paths updated; the "promote to the numbered
  inventory" item is now closed.
* Root OVERVIEW.md: added as #16, removed from the _wip skeleton table
  (20 -> 19), added a ### 16 detail section, and its vector/mapping notes moved
  out of the skeleton section.

All 16 top-level projects verified byte-identical.

Add Pass 1 verification tool; teach nesdis.py the undocumented opcodes

_tools/verify_pass1.py verifies Pass 1 across every top-level project in two
independent directions:

  * Faithful  - does the source still rebuild its ROM byte-identical?
  * Complete  - does the committed source claim as much code as a fresh trace
                can actually reach?

The second is the one that catches real problems. A project can reproduce all
16,384 bytes perfectly while calling 95% of them data, because the tracer gave
up early. Instruction *counts* are compared rather than bytes, so the check is
independent of comment style (which is not uniform across the repo).

Result: all 16 projects are faithful, and Joypad Test Cartridge is the only
truncated Pass 1 -- 282 committed instructions against 1,773 reachable.

nesdis.py gains three fixes needed to make that verdict trustworthy:

* --undocumented (default OFF) decodes the 105 undocumented NMOS opcodes.
  They are used as NOP padding and as real RMW ops, and a tracer that halts on
  them loses most of the code: Joypad goes 580 -> 3,846 code bytes. Default off
  so MUSCLE's `make regen` still reproduces prg.s byte-for-byte.

* Undocumented opcodes are emitted as explicit `.byte` with the disassembly in
  the comment. ca65 V2.19 implements none of them, so they cannot be written as
  mnemonics; keeping them in the code region preserves the code/data split and
  the audit trail. Official 0xEA NOP still encodes normally.

* Two latent bugs, both now reachable:
  - RESET and IRQ are commonly the same address, and the semantic-name map was
    overwriting, renaming the primary entry point to `IrqHandler`. Now
    setdefault, in RESET/NMI/IRQ priority order.
  - render() stamped every branch operand as Label_XXXX and never reconciled it
    with the semantic name map, so a branch into a named routine emitted a label
    that was never defined and the file would not assemble.

Joypad Test Cartridge (U): redo Pass 1, complete Pass 2; merge _wip into the inventory

Pass 1 (redone). The committed split claimed 282 instructions / 580 bytes (3.5%).
The tracer halted at $C09E on `DCP $3C20` ($CF) -- an undocumented opcode used as
ordinary code -- and filed 3,256 bytes of real code as `.byte` data.

Now 1,773 instructions / 3,836 bytes (23.4%), a 6.6x increase. Verified against a
fresh trace: 1,769 emitted instructions + 4 undocumented opcodes written as
explicit bytes = 1,773, an exact match. Byte accounting stays exact at
3,836 + 12,548 = 16,384.

ca65 V2.19 implements none of the undocumented opcodes, so the 4 affected
instructions (DCP $3C20, LAX $20D4, NOP $C1,X, RRA ($D0),X) are emitted as
explicit `.byte` with the disassembly in the comment. They stay in the code
region, so the split and the audit trail are both preserved.

Pass 2 (complete). All 202 labels renamed; zero `Label_Cxxxx` remaining. Renames
are keyed by address and verified to have bound to the intended address, not just
to some address. 202 routine headers, every one describing real behaviour -- no
"undetermined in Pass 1" boilerplate left. `locals.inc` carries 99 aliases, the
proven semantic ones sitting beside the generated `ZpXX` entries per the alias
pattern in AGENTS.md.

Two naming errors in the previous draft are corrected: $55 is MarkerValue (it
holds $70/$88/$A0 and is compared against them), not a "P1 Down timer"; and the
twelve MarkerVar variables are named by position because which player/button
each belongs to is not established.

Pass 3 is now outstanding, and the old Pass 3 was wrong: it described
$C09E-$C173 and $C17F-$CCF6 as data, and both are code. After the redo there is
exactly one data region, $CF06-$FFFF, confirmed by orphan scan.

Documentation.

The root inventory now lists all 36 projects in one table -- the 20 `_wip/`
projects are rows 17-36 rather than a second table that could drift. That second
table had wrong mirroring for 12 of them (Duck Hunt, Golf, Hogan's Alley, Ice
Climber, DK Jr Math, Lode Runner, Lunar Pool, Mario Bros, Millipede, Pyramid,
Raid [a2], Urban Champion were all recorded H when they are V, and vice versa);
every value is now read from the ROM rather than trusted.

Build status is split into two honest tiers: rows 1-16 are BYTE-IDENTICAL and
compare the whole ROM, rows 17-36 are PRG-IDENTICAL because their Makefiles only
run `cmp reconstructed_prg.bin prg.bin`. Header and CHR are not proven for those.
All 36 build cleanly; the WIP count was also corrected from 19 to 20.

`_tools/verify_pass1.py` checks all 36 and reports none truncated.

Mario Bros (JU): Pass 1 disassembly, 14.6% -> 72.2% code

The skeleton prg.s was a 12-line .incbin of the whole PRG. It is now a real
disassembly: 5,735 instructions, 11,829 code bytes (72.2%), 937 labels and
routine headers, byte accounting exact at 11,800 + 4,584 = 16,384, and
BYTE-IDENTICAL against prg.bin.

Coverage came from four steps, not one:

  vectors + direct jsr/jmp/branch, official opcodes   1,185 instr   14.6%
  --undocumented (105 extra NMOS opcodes)            1,210 instr   14.9%
  --auto-seed (61 table-referenced entries)          4,187 instr   51.1%
  --extra-seed 0xC0B6,0xC5A3                         5,735 instr   72.2%

Almost everything sits behind computed dispatch. Three mechanisms are now
documented in the project OVERVIEW:

* $CD9E is a stack-based indirect call: it pops the table address with `pla`,
  indexes it by A*2 with `lda ($14),y`, then `jmp ($0014)`. Because the pointer
  arrives on the stack it cannot be followed statically from the dispatcher.
* $C5A3 is a 20-entry table of `JSR` thunks ending in `rts`. Probing it linearly
  gives twenty valid targets, and seeding it recovers most of the last 21%.
* $C0B6 is the NMI phase dispatcher, reachable only by computed dispatch.

Two overlapping-decode bugs in _tools/nesdis.py had to be fixed first:

* Control-flow targets must win over fall-through. `bne` at $C4BA targets $C528,
  but the fall-through path had decoded $C527 as a 2-byte `NOP #$A5` whose
  operand swallowed $C528. No assembler branches into the middle of an
  instruction, so the spanning decode is now dropped in favour of the target;
  its bytes fall out as data, leaving byte accounting unchanged.
* Branch targets inside a surviving instruction must be pruned from the target
  set. Otherwise emit_code's linear walk consumes those bytes as an operand, so
  the label is referenced but never defined and the file does not assemble at
  all -- `Error: Symbol 'Label_C528' is undefined`.

Neither change disturbs an existing pipeline: MUSCLE's `make regen` still
reproduces prg.s byte-for-byte, Joypad Test Cartridge still builds byte-identical,
and _tools/verify_pass1.py reports all 16 top-level projects faithful with none
truncated.

Mario Bros (JU): Pass 2, partial -- make the call graph readable

Two mechanical wins, in order of value:

* **477 jsr/jmp operands rewritten from raw $XXXX to labels.** The tracer emits
  call and jump targets as hex, so the call graph was unreadable -- the most
  called routine appeared as `jsr $CCA0` 14 times over. Every one of those sites
  already had a label at its target address, so the substitution is purely
  mechanical and byte-preserving. Zero raw hex call operands remain.

* **13 landmark routines named** from verified instruction evidence:
  WaitVblank1, ClearRamPageLoop, NmiPhaseDispatch ($C0B6), JsrThunkTable
  ($C5A3), DispatchViaStackPointer ($CD9E), AdvanceDispatchPointer ($CDB4),
  PushTablePointerDispatch ($CCA0), DispatchTableF393 ($CAB9),
  InitSpritesOffscreen ($DFC4), AdjustDecimalColumns ($D15A),
  TestRamFlagPair0500 ($D328), NextIndex ($CE84).

**922 labels remain unnamed, and that is recorded as the honest state** rather
than papered over. They sit 15-25 per page across $CC00-$CFFF, which is just
the game logic: naming them means reading ~5,700 instructions across 935
routines. Guessing from the call graph and the NES convention would be exactly
the guessy rewrite PROMPTS.md rules out.

The productive next step is more reach, not more naming. The 4,555 unreached
bytes are where most unnamed routines live, and they are gated behind the
stack-pointer dispatch at $CD9E, whose pushed table addresses are only known in
the callers' frames.

Still BYTE-IDENTICAL; all 16 top-level projects remain faithful.

Record audit-comment work for Excitebike and Pac-Man as a TODO

Both projects are hand-written sources with semantic labels but no
byte-identity audit comment, so a source line cannot be mapped back to exact
ROM bytes without re-assembling. Every other project in the repo carries one.

Documented what was tried and why it was dropped rather than left implicit.
Addresses from 'ca65 -l' work fine -- the listing offset indexes prg.bin
directly -- but the listing carries no source line numbers, so entries cannot
be keyed back to source lines. Byte values also have to come from prg.bin
rather than the listing, since ca65 prints 'rr rr' for relocatable words.

No tool was committed, because the one written does not work. Both projects
verified still byte-identical; no source file was changed.

Add TODO.md handoff; bring AGENTS.md back in line with the repo

AGENTS.md had drifted on facts that are now wrong, not just incomplete:

* "Eleven disassemblies" -> 36 (16 top-level, 20 in _wip). The index it points
  at had the same stale count.
* The audit-comment convention was stated as `; PRG:$xxxx AA BB`, but the
  dominant spelling is `; CPU:$xxxx PRG:xxxx  AA BB` and five other variants are
  in use, with Excitebike and Pac-Man carrying none at all. Now stated accurately
  as "preserve whatever spelling a file uses", with the gap pointed at.
* `_tools/` was described as holding splitnes.py and chr2gif.py. It holds six
  tools now, including the Pass 1 tracer and the verifier.
* ROM filenames "contain spaces" -> spaces *and parentheses*, which is also what
  makes FCEUX hang headless.

Added, because each one costs real time if rediscovered:

* The two build tiers. The 20 _wip projects only `cmp reconstructed_prg.bin
  prg.bin`, so their header and CHR are NOT proven -- calling them
  byte-identical to the ROM is wrong.
* A promotion procedure, since that has now been done twice.
* A tracer/toolchain gotchas section: undocumented opcodes truncating traces
  (and ca65 being unable to encode them), vectors living at the end of the PRG
  rather than in the iNES header, the RESET==IRQ name collision, control-flow
  targets beating fall-through, the branch-target pruning that stops a file
  failing to assemble, NMI firing once per frame rather than per $2002 read,
  joy1 being $4016, and ca65 listings carrying no source line numbers.

TODO.md is the handoff document: open work per project with enough detail to
resume, the residue counts, and the reasoning behind each abandoned approach so
it is not silently retried.

Nothing rebuilt or changed; all projects still verify.

Joypad Test Cartridge (U): Pass 3 complete — data region named and VRAM command stream decoded

The monolithic `.byte $CF06-$FFFF` block (12,538 bytes) has been broken into 20 named sub-blocks:

VRAM command streams ($CF06-$D4AD): three command streams separated by $FF terminators, read by VramWriteLoop via RetAddrUpdate. Opcode set decoded: $4C = jump, $60 = deferred PPUCTRL, bit-7-clear = VRAM write (PPU addr + data, bit 6 of data = long/short selector).

OAM tables ($D4E3-$D6A3): 12 groups of 32-byte tables (8 entries of Y/tile/attr/X), read by CopyOamFromTable ($D4E3) and CopyOamFromTable70 ($D5C4).

Sprite templates ($D6A5-$D6E4): sprite template read by CopySpriteTemplateTable ($D6A5) and button OAM template read by PrepareButtonOam ($D6C5).

Padding: $FF ROM padding ($D6E5-$FFF9) and vectors ($FFFA-$FFFF).

OVERVIEW.md updated: section 8 documents all 20 sub-blocks with the VRAM command stream format and OAM table structure; section 11 marks Pass 3 complete with remaining work (MarkerVar player/button mapping, HandleButtonPress sprite order).

Build verified byte-identical.

Root OVERVIEW.md: mark Joypad Pass 3 complete

Update summary table (row 14: Pass Pass 2 → Pass 3 complete), detail section
(section 14: Pass 2/3 → Pass 3 complete with VRAM command stream decoded),
methodology note table (Pass 2 complete → Pass 3 complete), and notable
observations (Joypad at Pass 2/3 → Pass 3 complete).

Joypad Test Cartridge is now fully Pass 3 complete: all 20 data sub-blocks
named, VRAM command stream opcode set decoded, OAM table structure documented.

TODO.md: mark Joycad Test Cartridge Pass 3 complete

Mario Bros (JU): Pass 2 substantial — 252 subroutine entry points named

All 252 jsr/jmp targets resolved from raw $XXXX to labels:
- 90 semantic names: audio/sound routines ($F8xx-$F9xx: AudioPulse1CtrlSetup,
  WritePulse2Freq, ProcessTriangleChannel, SetupAudioChannel, etc.), gameplay
  helpers (CheckZpB3Return, CheckRangeReturn012, ToggleZpFC_Bit3, etc.)
- 165 address-based names (SubCxxxx) for the remainder
- Zero raw $XXXX call operands remain

667 Label_Cxxxx internal branch targets remain — these are local control-flow
destinations within routines, not subroutine entry points, and do not need
semantic names for the call graph to be readable.

Build verified BYTE-IDENTICAL. Root OVERVIEW.md updated (row 26: Pass 0 skeleton
→ Pass 1 complete, Pass 2 substantial). TODO.md updated.

Lunar Pool (U): Pass 1 complete, 57.8% code; nesdis.py opcode/emitter fixes

The skeleton prg.s was a 12-line .incbin of the whole PRG. It is now a real
disassembly: 4,586 instructions, 9,475 code bytes (57.8%), 6,909 data bytes,
byte accounting exact at 9,403 + 6,981 = 16,384, BYTE-IDENTICAL.

Coverage: vectors + direct jsr/jmp/branch reached 1,259 instructions (15.6%).
--undocumented took it to 4,572 (57.6%), and --auto-seed (48 table-referenced
entries) plus --extra-seed 0xE80C (the non-$C000 RESET vector) took it to
4,586 (57.8%).

Three undocumented opcodes were missing from nesdis.py and are added:
* $72 RRA abs (the RRA family was missing the $72 absolute form)
* $42 NOP #imm (the NOP-immediate family was missing $42)
* $FA NOP abs,X (the NOP-abs,X family was missing $FA)

One emitter fix: ca65 silently rewrites an operand whose high byte is $00 in
the shorter zero-page form -- `sta $00FF,x` assembles as `sta $FF,x` ($95
instead of $9D). render() now emits the `a:` prefix for ABS/ABSX/ABSY/IND
operands with a high byte of $00, forcing the absolute form so the emitted
bytes match the ROM. operand_value() strips the prefix when parsing.

These changes are general and verified not to disturb existing pipelines:
MUSCLE's `make regen` still reproduces prg.s byte-for-byte.

Remaining gaps: 6,909 bytes (42.2%) still data, gated behind jmp ($7961) at
three sites (RAM-pointer dispatch; the blocks after them hold the screen text
"strings" "game over" and "hi`score__") and stack-based dispatch at $CBDC --
the same pattern as Mario Bros (JU)'s $CD9E.

TODO.md: add Lunar Pool as an open item

Adds comments for all lines to Pacman and Excitebike
Moves Pacman 'locals' and 'symbols' definitions into the right places
This commit is contained in:
tmerritt committed 2026-10-17 17:36:28 -04:00
1 parent e9caa8cb36
commit 837007843d
50 files changed
+62939 -13111

No files matched your search

+89 -16
View File
@@ -13,7 +13,7 @@ This document describes the execution flow of the disassembled Joypad Test Cartr
| CHR | 1 × 8 KB |
| Mirroring | Horizontal |
| PRG bank start | CPU $C000 |
| Source file | `copilot/prg.s` (~1,360 lines with headers) |
| Source file | `copilot/prg.s` (4,516 lines with headers) |
| Output | `reconstructed.nes` (24,592 bytes) |
### Interrupt vectors (CPU $FFFA–$FFFF)
@@ -30,7 +30,7 @@ This document describes the execution flow of the disassembled Joypad Test Cartr
`make` in `copilot/` assembles `prg.s` with `ca65`, links with `ld65`, and verifies against `prg.bin`.
**Verification result: BYTE-IDENTICAL** — `reconstructed_prg.bin` matches `prg.bin` exactly (all 16,384 bytes), and `prg.bin` matches the PRG bank of `Joypad Test Cartridge (U).nes`.
**Verification result: BYTE-IDENTICAL** — `make` compares the whole reconstructed ROM against `copilot/rom.nes` and prints `Byte-identical ROM verified.` Byte accounting is exact: 3,836 code + 12,548 data = 16,384 of 16,384 bytes.
---
@@ -38,9 +38,26 @@ This document describes the execution flow of the disassembled Joypad Test Cartr
| Pass | Status | Description |
|------|--------|-------------|
| Pass 1 | Complete | Code/data split — ~580 bytes traced code (~3.5%) or explicit `.byte` data (~96.5%) with `; CPU:$xxxx PRG:$xxxx` annotations |
| Pass 2 | Complete | Routine headers added for all labeled positions; key routines semantically named (`Reset`, `NmiHandler`, `VramWrite`, `PpuScrollLatch`, etc.); zero-page aliases in `locals.inc` |
| Pass 3 | Complete | Data regions identified and documented (VRAM burst write table at $0302, pointer table at $0000, test pattern/button mapping data in $CE98–$FFFF); remaining `Label_Cxxxx` references are internal branch targets within routines |
| Pass 1 | **Redone** | Was badly truncated: 282 instructions / 580 bytes (3.5%). Now **1,773 instructions / 3,836 bytes (23.4%)** — a 6.6x increase. Root cause: the tracer only knew the 151 official opcodes and halted at `$C09E` on `DCP $3C20` (`$CF`), filing 3,256 bytes of real code as `.byte`. Verified against a fresh trace: 1,769 emitted instructions + 4 undocumented opcodes written as explicit bytes = 1,773, an exact match |
| Pass 2 | **Complete** | 202 labels, **zero `Label_Cxxxx` remaining**; 202 routine headers, all describing real behaviour (no "undetermined in Pass 1" boilerplate); `locals.inc` carries 99 aliases, half of them proven semantic names sitting beside the generated `ZpXX` entries |
| Pass 3 | **Complete** | The single data region `$CF06–$FFFF` has been broken into 20 named sub-blocks: VRAM command streams read by `VramWriteLoop`/`RetAddrUpdate`, seven 32-byte OAM table groups read by `CopyOamFromTable`/`CopyOamFromTable70`, the 32-byte sprite template read by `CopySpriteTemplateTable`, the 32-byte button OAM template read by `PrepareButtonOam`, and 10,517 bytes of `$FF` ROM padding. The `RetAddrUpdate` VRAM command stream opcode set has been decoded (`$4C` = jump, `$60` = deferred PPUCTRL, bit-7-clear bytes = VRAM write with PPU addr + data, bit 6 of data = long/short command selector). |
### Why Pass 1 was truncated
The tracer descended from the three vectors, following direct `jsr`/`jmp`/branch
targets. It stopped at `$C09E`, where the byte is `$CF` = `DCP $3C20` — an
undocumented read-modify-write op used here as ordinary code. Everything after
that point was written out as data until a later run promoted it.
This cartridge is an unusually clear demonstration: nothing exotic is involved,
just one undocumented opcode in the middle of the main loop. `nesdis.py` now
takes `--undocumented` (default **off**, so other projects' `make regen` stays
byte-stable) to decode all 105 undocumented NMOS opcodes.
ca65 V2.19 implements none of them, so the 4 affected instructions
(`DCP $3C20`, `LAX $20D4`, `NOP $C1,X`, `RRA ($D0),X`) are emitted as explicit
`.byte` with the disassembly in the comment. They stay in the code region, so
the split and the audit trail are both preserved.
---
@@ -144,17 +161,64 @@ Full list in `copilot/locals.inc`.
## 8. Data Regions
The PRG is approximately 3.5% code / 96.5% data. The data regions include:
The PRG is **23.4% code / 76.6% data** (3,836 / 12,548 bytes). The data region `$CF06–$FFFF` has been broken into 20 named sub-blocks:
- **$C09E–$C173** (214 bytes): Main loop inline code/data (pointer table references, VRAM write parameters)
- **$C17F–$CCF6** (2,936 bytes): Test patterns, button mapping tables, screen data
- **$CD39–$CD3E** (6 bytes): PPU mask toggle code fragment
- **$CE98–$FFFF** (12,648 bytes): Test pattern data, button mapping tables, screen layout data
| Address | Size | Name | Contents |
|---------|------|------|----------|
| `$CF06–$CF28` | 35 B | `VramCommandStream1` | VRAM command stream start — PPU addr/data pairs read by `VramWriteLoop` |
| `$CF29` | 1 B | `VramStream1Terminator` | `$FF` terminator ending first command stream |
| `$CF2A–$CF3F` | 22 B | `VramCommandInlineCode` | Inline code after `jsr $CE98` — `lax $20D4`, `nop $C1,X`, `$61` padding |
| `$CF40–$D061` | 290 B | `VramCommandBlock2` | VRAM command/data block — PPU writes and OAM attribute patterns |
| `$D062` | 1 B | `VramStream2Terminator` | `$FF` terminator ending second command stream |
| `$D063–$D1F3` | 401 B | `VramCommandBlock3` | VRAM command/data block — continues command stream patterns |
| `$D1F4` | 1 B | `VramStream3Terminator` | `$FF` terminator ending third command stream |
| `$D1F5–$D4AD` | 697 B | `VramCommandBlock4` | Largest VRAM command/data block — 697 bytes of command stream |
| `$D4AE` | 1 B | `OamTableStreamTerminator` | `$FF` terminator before OAM gap data |
| `$D4AF–$D4E1` | 51 B | `OamTableGap` | Gap data before OAM table at `$D4E3` |
| `$D4E2` | 1 B | `OamTable1Terminator` | `$FF` terminator before OAM table group 1 |
| `$D4E3–$D5C2` | 224 B | `OamTableGroups1` | OAM table groups 1-7 (`CopyOamFromTable` reads `$D4E3`) — 7×32 byte tables, each with 8 entries of Y/tile/attr/X |
| `$D5C3` | 1 B | `OamTableStream2Terminator` | `$FF` terminator before second OAM table group |
| `$D5C4–$D603` | 64 B | `OamTableGroups2` | OAM table groups 1-2 (`CopyOamFromTable70` reads `$D5C4`) — 2×32 byte tables |
| `$D604–$D643` | 64 B | `OamSpritePadding` | 64 bytes of `$F4` sprite off-screen padding |
| `$D644–$D6A3` | 96 B | `OamTableGroups3` | OAM table groups 3-5 — 3×32 byte tables before sprite template |
| `$D6A4` | 1 B | `SpriteTableTerminator` | `$FF` terminator before sprite template table |
| `$D6A5–$D6C4` | 32 B | `SpriteTemplateTable` | Sprite template — 8×4 Y/tile/attr/X entries (`CopySpriteTemplateTable` reads `$D6A5`) |
| `$D6C5–$D6E4` | 32 B | `ButtonOamTemplateTable` | Button OAM template — 8×4 Y/tile/attr/X entries (`PrepareButtonOam` reads `$D6C5`) |
| `$D6E5–$FFF9` | 10,517 B | `RomPadding` | `$FF` ROM padding |
| `$FFFA–$FFFF` | 6 B | `ResetVectors` | Reset/NMI/IRQ vectors |
Notable data structures:
- Pointer table at $0000–$0001 (read by `VramPtrTableRead` at $CD99)
- VRAM burst write table at $0302 (read by `VramBurstWrite` at $CE4F)
- Button mapping / test pattern tables in the $C000–$FFFF range
The previous Pass 3 claimed four data regions totalling ~15.8 KB, including `$C09E–$C173` and `$C17F–$CCF6`. Both are code. Those claims were an artefact of the truncated Pass 1 and have been removed.
### VRAM Command Stream Format
The VRAM command stream is read by `VramWriteLoop` (`$CD45`), dispatched through `VramPtrTableRead` (`$CD99`) and `RetAddrUpdate` (`$CDC8`). The stream uses the `RetAddrUpdate` return-address modification trick: `jsr $CE98` (`DispatchVramOp`) pushes a return address, `RetAddrUpdate` advances it by 2 to skip the command byte, and execution resumes inline after the dispatch returns.
Command byte format (read by `DispatchVramOp` at `$CE98`):
- **Low 4 bits**: data length (number of bytes following)
- **High 4 bits**: command type
Opcodes:
- `$4C` (`HandleJmpOpcode`): jump — reads 2-byte address from stream, rewrites dispatch pointer
- `$60` (`HandlePpuCtrlOpcode`): deferred PPUCTRL write — pops return address from stack as new stream pointer
- Bit 7 clear, not `$4C`/`$60` (`VramWriteLoop`): VRAM write — opcode byte = PPU addr high, next byte = PPU addr low, then data bytes. Bit 6 of each data byte selects short (1 byte) vs long (multiple bytes) command.
- Bit 7 set: end of stream
### OAM Table Structure
OAM tables consist of 8 entries of 4 bytes each (Y, tile, attr, X), matching the NES OAM layout:
- `SpriteTemplateTable` (`$D6A5`): Y=$79 for all entries, player sprite tiles
- `ButtonOamTemplateTable` (`$D6C5`): Y=$79 for all entries, button indicator tiles
- `OamTableGroups1` (`$D4E3`): 7 groups with varying Y values ($37, $67, $57, $97, $87, $C7, $B7, $5F, $6F)
- `OamTableGroups2` (`$D5C4`): 2 groups with Y values $37 and $67
- `OamTableGroups3` (`$D644`): 3 groups with Y values $27, $37, $57, $67
Notable data structures (addresses from the code that reads them):
- VRAM pointer at `$00/$01`, read indirectly by `VramPtrTableRead` ($CD99), `WriteVramStreamByte` ($CD74) and `DispatchVramOp` ($CE98)
- VRAM burst command table at `$0302`, read by `VramBurstWrite` ($CE4F) and written by `StoreVramOpResult` ($CEB2); `SetBurstWriteIndex` primes it with `$22`
- VRAM op length limit at `$0300`, tested by `VramOpLengthCheck` ($CEF3)
- Sprite template tables at `$D6A5` (`CopySpriteTemplateTable`) and `$D6C5` (`PrepareButtonOam`), copied into the OAM shadow at `$02E0`
- Further OAM tables at `$D4E3` (`CopyOamFromTable`) and `$D5C4` (`CopyOamFromTable70`)
---
@@ -173,10 +237,19 @@ make # → ca65 + ld65 + cmp reconstructed.nes rom.nes → "Byte-identica
- `RetAddrUpdate` ($CDC8) implements a return-address modification trick: it reads the return address from the stack, advances it by 2, and stores it back. This is used with `VramPtrWrite` ($CD3F) to implement runtime-computed control flow (effectively a coroutine/threaded dispatch).
- The IRQ vector points to $C000 (Reset) and is effectively unused.
- Large portions of the PRG are test pattern and button mapping data, not executable code.
- `PollExpansionPortLoop` reads `$4017` and compares bits 0-1, but nothing consumes
the result — the branch is always taken the same way. The read is present but inert.
- `MarkerVar01`–`MarkerVar12` (zero page `$5A`, `$5B`, `$56`–$5F`, `$60`, `$61`, `$63`,
`$64`) drive the twelve `MarkerUpdateN` indicator groups. Which player and button each
belongs to is **not** established, so they are named by position rather than by button.
- A draft Pass 2 `locals.inc` called `$55` a "P1 Down timer". The instructions do not
support that: `ApplyMarkerValueEnableAudio` stores `$70`/`$88`/`$A0` there and
`DispatchOnMarkerValue` compares it against `$70` and `$88`. It is `MarkerValue`.
---
## 11. Next Steps
- **Promotion complete** — moved to top-level `Joypad Test Cartridge (U)/` folder with full ROM build (`header.s`, `chr.s`, `linker.cfg`, `Makefile`).
- **Optional deeper Pass 3 work** — extract button mapping and test pattern tables from untraced regions into labeled `.byte` blocks; refine remaining zero-page aliases.
- **Pass 3 is complete.** All 20 data sub-blocks are named (section 8), the VRAM command stream opcode set is decoded, and the OAM table structure is documented.
- Establish which player/button each `MarkerVar` belongs to. This needs the OAM layout for the indicator sprites and the button bit order confirmed against the shift-register read path, not just assumed from the NES convention.
- `HandleButtonPress1`–`HandleButtonPress8` write OAM tiles in the order `$0202, $0206, $020E, $020A, $0212, $0216, $021A, $021E` — sprites 1, 2, 4, 3, 5, 6, 7, 8. The out-of-order third and fourth entries are unexplained; resolving them would confirm which bit maps to which button.
+146 -43
View File
@@ -1,46 +1,149 @@
; Joypad Test Cartridge (U) zero-page aliases - Pass 2
; Semantic names for addresses with observed usage patterns; ZpXX names for
; addresses whose role is not yet uniquely determined.
; Joypad Test Cartridge (U) application-specific RAM aliases - Pass 1
; Generated from every direct zero-page operand in reachable code:
; ref counts are reads/writes, `where` lists the referencing addresses.
; ZpXX names indicate storage with a role not yet unique enough for a behavioral name;
; Pass 2 replaces them with names derived from observed usage.
VramPtrLo = $00 ; VRAM write pointer low byte (indirect, used by VramWriteLoop/VramWrite)
VramPtrHi = $01 ; VRAM write pointer high byte (indirect)
VramLen = $02 ; VRAM write data length counter
PpuScrollX = $03 ; PPU horizontal scroll value (latched to $2005)
Zp04 = $04 ; Temporary zero-page storage
RetAddrTempLo = $05 ; Return address temp low byte (used by RetAddrUpdate)
RetAddrTempHi = $06 ; Return address temp high byte (used by RetAddrUpdate)
Zp0D = $0D ; Temporary zero-page storage
PpuMaskShadow = $0E ; PPUMASK shadow register (bits cleared for sprite/bg)
PpuScrollY = $0EC ; PPU vertical scroll value (latched to $2005)
PpuScrollX_Init = $0ED ; PPU horizontal scroll value written during init
PpuMaskValue = $0EE ; PPUMASK value written during init (also shadow)
PpuCtrlShadow = $EF ; PPUCTRL shadow register (NMI bit cleared via and #$7F)
VramToggleFlag = $CF ; Flag toggled each frame (asl/rol at $C095-$C09D)
NmiFlagByte = $E0 ; Flag cleared during NMI ($CD0F)
Zp00 = $00 ; 22 refs; reads=13 writes=9; used at $C013, C05D, C252, CB16, +18 more
Zp01 = $01 ; 9 refs; reads=4 writes=5; used at $CD95, CD97, CDA8, CDB8, +5 more
Zp02 = $02 ; 7 refs; reads=4 writes=3; used at $CDF2, CE10, CE1F, CE9A, +3 more
Zp03 = $03 ; 4 refs; reads=2 writes=2; used at $CE98, CEB2, CEE5, CEE7
Zp04 = $04 ; 2 refs; reads=1 writes=1; used at $CEA5, CEC2
Zp05 = $05 ; 7 refs; reads=4 writes=3; used at $CDCC, CDD5, CDDA, CDE1, +2 more
Zp06 = $06 ; 5 refs; reads=2 writes=3; used at $CDD1, CDE8, CEC4, CED2
Zp40 = $40 ; 57 refs; reads=32 writes=25; used at $C0E7, C0E9, C0EB, C0EF, +30 more
Zp41 = $41 ; 3 refs; reads=3 writes=0; used at $C1BB, C304, CBB7
Zp42 = $42 ; 2 refs; reads=0 writes=2; used at $CCCE, CCDC
Zp43 = $43 ; 2 refs; reads=2 writes=0; used at $C1BF, C308
Zp44 = $44 ; 3 refs; reads=3 writes=0; used at $C1C3, C30C, CCCC
Zp45 = $45 ; 2 refs; reads=1 writes=1; used at $CC75
Zp46 = $46 ; 1 refs; reads=1 writes=0; used at $CCDA
Zp47 = $47 ; 2 refs; reads=1 writes=1; used at $CC72
Zp49 = $49 ; 3 refs; reads=2 writes=1; used at $CC88, CC94
Zp4A = $4A ; 1 refs; reads=1 writes=0; used at $CCA0
Zp4B = $4B ; 3 refs; reads=2 writes=1; used at $CC85, CCAC
Zp4C = $4C ; 1 refs; reads=1 writes=0; used at $CCB8
Zp4D = $4D ; 10 refs; reads=5 writes=5; used at $CC92, CC9C, CC9E, CCA8, +6 more
Zp4E = $4E ; 40 refs; reads=19 writes=21; used at $C212, C2BC, C331, C3A3, +27 more
Zp4F = $4F ; 4 refs; reads=2 writes=2; used at $C2BE, C335, CB74, CBD9
Zp51 = $51 ; 3 refs; reads=1 writes=2; used at $C2C0, C339, CB76
Zp52 = $52 ; 3 refs; reads=1 writes=2; used at $C2C2, C33D, CB78
Zp53 = $53 ; 13 refs; reads=6 writes=7; used at $C0DB, C0FD, C107, C2C6, +6 more
Zp54 = $54 ; 13 refs; reads=6 writes=7; used at $C0DD, C0F9, C101, C2C4, +6 more
Zp55 = $55 ; 8 refs; reads=6 writes=2; used at $C0D7, C113, C119, C11F, +4 more
Zp56 = $56 ; 21 refs; reads=12 writes=9; used at $C2CC, C371, C37D, C60A, +15 more
Zp57 = $57 ; 16 refs; reads=10 writes=6; used at $C2D2, C763, C769, C76E, +12 more
Zp59 = $59 ; 14 refs; reads=10 writes=4; used at $C8BD, C8C3, C8C8, C8CF, +10 more
Zp5A = $5A ; 14 refs; reads=10 writes=4; used at $CA16, CA1C, CA21, CA28, +10 more
Zp5B = $5B ; 21 refs; reads=12 writes=9; used at $C2CA, C3B6, C3C4, C597, +15 more
Zp5C = $5C ; 16 refs; reads=10 writes=6; used at $C2D0, C6F0, C6F6, C6FB, +12 more
Zp5E = $5E ; 14 refs; reads=10 writes=4; used at $C84A, C850, C855, C85C, +10 more
Zp5F = $5F ; 14 refs; reads=10 writes=4; used at $C9A3, C9A9, C9AE, C9B5, +10 more
Zp60 = $60 ; 21 refs; reads=12 writes=9; used at $C2C8, C429, C439, C524, +15 more
Zp61 = $61 ; 16 refs; reads=10 writes=6; used at $C2CE, C67D, C683, C688, +12 more
Zp63 = $63 ; 14 refs; reads=10 writes=4; used at $C7D7, C7DD, C7E2, C7E9, +10 more
Zp64 = $64 ; 14 refs; reads=10 writes=4; used at $C930, C936, C93B, C942, +10 more
Zp65 = $65 ; 40 refs; reads=18 writes=22; used at $C2D4, C36D, C379, C380, +18 more
Zp66 = $66 ; 3 refs; reads=0 writes=3; used at $C2D6, CAAA, CB8C
Zp68 = $68 ; 3 refs; reads=0 writes=3; used at $C2D8, CAAC, CB8E
Zp69 = $69 ; 3 refs; reads=0 writes=3; used at $C2DA, CAAE, CB90
Zp6A = $6A ; 9 refs; reads=4 writes=5; used at $C2B8, C2EA, C2EE, CA8C, +3 more
ZpC1 = $C1 ; 2 refs; reads=1 writes=1; used at $C2A3
ZpCF = $CF ; 6 refs; reads=3 writes=3; used at $C095, C287, CB4B
ZpD0 = $D0 ; 2 refs; reads=1 writes=1; used at $CB53
ZpD1 = $D1 ; 1 refs; reads=1 writes=0; used at $C28F
ZpE0 = $E0 ; 3 refs; reads=1 writes=2; used at $CCEF, CCF2, CD0F
ZpEC = $EC ; 2 refs; reads=1 writes=1; used at $C04D, CE44
ZpED = $ED ; 2 refs; reads=1 writes=1; used at $C04B, CE3F
ZpEE = $EE ; 7 refs; reads=2 writes=5; used at $C03D, C084, C279, CB3D, +3 more
ZpEF = $EF ; 15 refs; reads=6 writes=9; used at $C036, C07D, C272, CB36, +11 more
; Controller / button state (indexed by button number via $4E,X)
JoyBtnStateBase = $4E ; Base address for indexed button states ($4E,X)
JoyBtnState_P1_A = $4E ; Player 1 button A state/timer
JoyBtnState_P1_B = $4F ; Player 1 button B state/timer
JoyBtnState_P1_Select = $50 ; Player 1 select state/timer (if used)
JoyBtnState_P1_Start = $51 ; Player 1 start state/timer
JoyBtnState_P1_Up = $52 ; Player 1 up state/timer
FrameCounterLo = $53 ; Frame counter low byte (incremented each frame)
FrameCounterHi = $54 ; Frame counter high byte
JoyBtnTimer_P1_Down = $55 ; Player 1 down button timer
JoyBtnTimer_P1_Left = $56 ; Player 1 left button timer
JoyBtnTimer_P1_Right = $57 ; Player 1 right button timer
JoyBtnTimer_P1_Start2 = $5C ; Player 1 start timer (second instance)
JoyBtnTimer_P1_A2 = $61 ; Player 1 button A timer (second instance)
JoyBtnTimer_P2_Down = $5E ; Player 2 down button timer
JoyBtnTimer_P2_Left = $59 ; Player 2 left button timer
JoyBtnTimer_P2_Right = $64 ; Player 2 right button timer
JoyBtnTimer_P2_A = $5F ; Player 2 button A timer
JoyBtnTimer_P2_B = $5A ; Player 2 button B timer
; ---------------------------------------------------------------------------
; Pass 2 semantic aliases
;
; Aliases sit beside the generated ZpXX names rather than replacing them, since
; several addresses are reused for more than one role. Each name below is
; justified by the referencing routines named in its comment.
; ---------------------------------------------------------------------------
; Generic zero-page aliases (role not yet uniquely determined)
Zp60 = $60 ; Temporary zero-page storage
Zp65 = $65 ; Temporary zero-page storage
Zp66 = $66 ; Temporary zero-page storage
Zp68 = $68 ; Temporary zero-page storage
Zp69 = $69 ; Temporary zero-page storage
; VRAM writer state. $00/$01 form the indirect pointer used by
; `lda (Zp00),y` in WriteVramStreamByte, VramPtrTableRead and DispatchVramOp.
VramPtrLo = $00
VramPtrHi = $01
VramOpLength = $02 ; byte count for the current VRAM op
VramOpResult = $03 ; DispatchVramOp / StoreVramOpResult
VramOpTemp = $04
RetAddrTempLo = $05 ; RetAddrUpdate pulls the caller's PC off the stack
RetAddrTempHi = $06
; Player 1 controller bits. Read by ReadControllerBitsLoop as
; `lda $4016,x / lsr a / rol Zp40,x` for x = 0..7, so $40 holds all eight
; buttons and is subsequently rotated one bit at a time by
; PollControllerShiftRegister and DispatchButtonBits.
P1Buttons = $40 ; bit0 A, 1 B, 2 Select, 3 Start, 4 Up, 5 Down, 6 Left, 7 Right
P1ButtonA = $40 ; aliases for the individual bit tests
P1ButtonB = $41
P1ButtonSelect = $42
P1ButtonStart = $43
P1ButtonUp = $44
P1ButtonDown = $45
P1ButtonLeft = $46
P1ButtonRight = $47
; Previous-direction latch written by LatchPreviousStickState (`sta Zp42,x`),
; read by CheckStickUp/Down/Left/Right against $10/$20/$C0 thresholds.
PrevStickState = $42
StickDiagonalState = $46
; Second and third shift registers from the alternate read paths
; (`rol Zp47,x` at ReadControllerBitsAltLoop, `rol Zp4B,x` at
; ReadControllerBitsAlt2Loop).
P1ButtonsAlt = $47
P2Buttons = $4B
ButtonShiftAlt = $65 ; second shift register in DispatchButtonBits
; Direction accumulators compared in CheckStickUp..Right
StickAccumUp = $4A
StickAccumDown = $4B
StickAccumLeft = $4C
StickAccumRight = $4D
; 16-bit frame counter: IncrementFrameCounter does
; `inc Zp54 / bne / inc Zp53`, so $53 is the high byte and $54 the low byte.
FrameCounterHi = $53 ; incremented on carry out of the low byte
FrameCounterLo = $54 ; incremented every frame
; Holds one of $70 / $88 / $A0. DispatchOnMarkerValue compares it against $70
; and $88 to choose a test mode; ApplyMarkerValueEnableAudio stores it to
; $0200 and to the shadow at $EE. NOTE: an earlier Pass 2 draft called this a
; "P1 Down timer", which the instructions do not support.
MarkerValue = $55
; Per-button-state marker variables. Each drives one group of
; MarkerUpdateN routines, which OR $F0 or $D0 into the variable and write the
; result to an OAM Y-coordinate slot. Which player and button each belongs to
; is NOT yet established, so they are named by position, not by button.
MarkerVar01 = $60 ; MarkerUpdate1
MarkerVar02 = $5B ; MarkerUpdate2
MarkerVar03 = $56 ; MarkerUpdate3
MarkerVar04 = $61 ; MarkerUpdate4
MarkerVar05 = $5C ; MarkerUpdate5
MarkerVar06 = $57 ; MarkerUpdate6
MarkerVar07 = $63 ; MarkerUpdate7
MarkerVar08 = $5E ; MarkerUpdate8
MarkerVar09 = $59 ; MarkerUpdate9
MarkerVar10 = $64 ; MarkerUpdate10
MarkerVar11 = $5F ; MarkerUpdate11
MarkerVar12 = $5A ; MarkerUpdate12
ButtonHoldCounter = $6A ; PollControllerCountFrames: inc / cmp #$0F
VramBurstIndex = $0302 ; SetBurstWriteIndex stores #$22 here
; PPU register shadows. $EF is read by WaitForVblank and masked with #$7F
; before reaching $2000; $EE is masked with #$E7 by PpuMaskSetup.
PpuScrollXInit = $ED ; latched to $2005 by PpuScrollLatch
PpuMaskShadow = $EE ; PPUMASK shadow
PpuCtrlShadow = $EF ; PPUCTRL shadow
; Set at $E0 and polled by `bit ZpE0 / bmi` in WaitForNmiComplete; NMI clears it.
NmiInProgressFlag = $E0
File diff suppressed because it is too large. Load diff
@@ -1,35 +1,202 @@
al 00CE89 .Label_CE89
al 00CE6E .Label_CE6E
al 00CE5F .Label_CE5F
al 00CE8A .Label_CE8A
al 00CF05 .VramOpReturn
al 00CEF3 .VramOpLengthCheck
al 00CEC9 .VramOpDataStream
al 00CEB2 .StoreVramOpResult
al 00CE98 .DispatchVramOp
al 00CE89 .VramBurstSkipRun
al 00CE6E .VramBurstWriteNext
al 00CE5F .VramBurstWriteChunk
al 00CE8A .VramBurstNextCommand
al 00CE4F .VramBurstWrite
al 00CE3C .PpuScrollLatch
al 00CE33 .Label_CE33
al 00CE39 .Label_CE39
al 00CE16 .Label_CE16
al 00CE12 .Label_CE12
al 00CE33 .WriteVramByteStream
al 00CE39 .VramWriteDone
al 00CE16 .WriteVramZeroFill
al 00CE12 .VramWriteSetup
al 00CDEE .VramWrite
al 00CDC8 .RetAddrUpdate
al 00CDB1 .Label_CDB1
al 00CDA3 .Label_CDA3
al 00CDB1 .HandleJmpOpcode
al 00CDA3 .HandlePpuCtrlOpcode
al 00CD99 .VramPtrTableRead
al 00CD8D .Label_CD8D
al 00CD74 .Label_CD74
al 00CD71 .Label_CD71
al 00CD6E .Label_CD6E
al 00CD69 .Label_CD69
al 00CD5C .Label_CD5C
al 00CD8D .AdvanceVramPointerByY
al 00CD74 .WriteVramStreamByte
al 00CD71 .AdvanceVramReadIndex
al 00CD6E .ComputeVramByteCount
al 00CD69 .ReturnFromVramPtrWrite
al 00CD5C .WritePpuCtrlFromShadow
al 00CD45 .VramWriteLoop
al 00CD3F .VramPtrWrite
al 00CD39 .EnableSpritesAndBackground
al 00CD33 .WritePpuMask
al 00CD2F .PpuMaskSetup
al 00CD24 .OamDmaSetup
al 00CD17 .PpuCtrlSetup
al 00CD0A .NmiContinue
al 00CCF7 .NmiHandler
al 00C178 .Label_C178
al 00C174 .ClearOAM_Routine
al 00C05D .ClearRAM2_Loop
al 00C013 .ClearRAM1_Loop
al 00CCF2 .WaitForNmiComplete
al 00CCE2 .WaitForVblank
al 00CCDA .LatchPreviousDiagonalState
al 00CCE1 .ReturnFromStickDecode
al 00CCCC .LatchPreviousStickState
al 00CCD4 .CheckStickDiagonal
al 00CCC4 .CheckStickRight
al 00CCB8 .CheckStickLeft
al 00CCAC .CheckStickDown
al 00CCA0 .CheckStickUp
al 00CC81 .ReadControllerBitsAlt2Loop
al 00CC7F .ReadControllerBitsAlt2Init
al 00CC6E .ReadControllerBitsAltLoop
al 00CC6C .ReadControllerBitsAltInit
al 00CC5B .PollExpansionPortLoop
al 00CC59 .PollExpansionPortInit
al 00CC4F .LatchControllerStrobeAlt
al 00CC46 .CopyOamShadowLoop
al 00CC18 .WriteOamFromButtonState
al 00CC11 .ReadControllerBitSingle
al 00CC04 .ReadControllerBitsLoop
al 00CC02 .ReadControllerBitsInit
al 00CBF8 .LatchControllerStrobe
al 00CBF5 .ExitToInit70
al 00CBF2 .ContinueTestMode70
al 00CBE6 .CheckFrameCounterLimit70
al 00CBE0 .IncrementFrameCounter70
al 00CBC0 .UpdateOamTestMode70
al 00CBB1 .CheckButtonsTestMode70
al 00CBAB .UpdateMarkersAndTimers70
al 00CB9A .MainLoopTestMode70
al 00CB5D .CopyOamFromTable70
al 00CB16 .ClearRamPass4Loop
al 00CB0A .WriteRamPattern70Loop
al 00CB03 .InitTestScreen70
al 00CB00 .ShiftButtonBit8Done
al 00CAF7 .ShiftButtonBit7
al 00CAEE .ShiftButtonBit6
al 00CAE5 .ShiftButtonBit5
al 00CADC .ShiftButtonBit4
al 00CAD3 .ShiftButtonBit3
al 00CACA .ShiftButtonBit2
al 00CAC1 .ShiftButtonBit1
al 00CAB8 .ShiftButtonBit0
al 00CAB1 .ResetButtonTimers
al 00CA8A .ResetButtonMarkerState
al 00CA89 .MarkerUpdate12_Return
al 00CA7D .MarkerUpdate12_Low
al 00CA6B .MarkerUpdate12_Mid
al 00CA56 .MarkerUpdate12_High
al 00CA16 .MarkerUpdate12
al 00CA0A .MarkerUpdate11_Low
al 00C9F8 .MarkerUpdate11_Mid
al 00C9E3 .MarkerUpdate11_High
al 00C9A3 .MarkerUpdate11
al 00C997 .MarkerUpdate10_Low
al 00C985 .MarkerUpdate10_Mid
al 00C970 .MarkerUpdate10_High
al 00C930 .MarkerUpdate10
al 00C924 .MarkerUpdate9_Low
al 00C912 .MarkerUpdate9_Mid
al 00C8FD .MarkerUpdate9_High
al 00C8BD .MarkerUpdate9
al 00C8B1 .MarkerUpdate8_Low
al 00C89F .MarkerUpdate8_Mid
al 00C88A .MarkerUpdate8_High
al 00C84A .MarkerUpdate8
al 00C83E .MarkerUpdate7_Low
al 00C82C .MarkerUpdate7_Mid
al 00C817 .MarkerUpdate7_High
al 00C7D7 .MarkerUpdate7
al 00C7D6 .MarkerUpdate6_Return
al 00C7CA .MarkerUpdate6_Low
al 00C7B8 .MarkerUpdate6_Mid
al 00C7A3 .MarkerUpdate6_High
al 00C763 .MarkerUpdate6
al 00C757 .MarkerUpdate5_Low
al 00C745 .MarkerUpdate5_Mid
al 00C730 .MarkerUpdate5_High
al 00C6F0 .MarkerUpdate5
al 00C6E4 .MarkerUpdate4_Low
al 00C6D2 .MarkerUpdate4_Mid
al 00C6BD .MarkerUpdate4_High
al 00C67D .MarkerUpdate4
al 00C671 .MarkerUpdate3_Low
al 00C65F .MarkerUpdate3_Mid
al 00C64A .MarkerUpdate3_High
al 00C60A .MarkerUpdate3
al 00C5FE .MarkerUpdate2_Low
al 00C5EC .MarkerUpdate2_Mid
al 00C5D7 .MarkerUpdate2_High
al 00C597 .MarkerUpdate2
al 00C58B .MarkerUpdate1_Low
al 00C579 .MarkerUpdate1_Mid
al 00C564 .MarkerUpdate1_High
al 00C524 .MarkerUpdate1
al 00C523 .ReturnFromPrepareButtonOam
al 00C520 .CallPrepareButtonOam
al 00C4FC .HandleButtonPress8
al 00C50A .FinishButtonDispatch
al 00C4D4 .HandleButtonPress7
al 00C4E2 .ButtonBit12_Test
al 00C4AC .HandleButtonPress6
al 00C4BA .ButtonBit11_Test
al 00C484 .HandleButtonPress5
al 00C492 .ButtonBit10_Test
al 00C45C .HandleButtonPress4
al 00C46A .ButtonBit9_Test
al 00C43C .ButtonBit8_Test
al 00C42F .ButtonBit7_Test
al 00C40D .HandleButtonPress3
al 00C41B .ButtonBit6_Test
al 00C3E5 .HandleButtonPress2
al 00C3F3 .ButtonBit5_Test
al 00C3C7 .ButtonBit4_Test
al 00C3BC .ButtonBit3_Test
al 00C39C .HandleButtonPress1
al 00C3AA .ButtonBit2_Test
al 00C380 .ButtonBit1_Test
al 00C377 .ButtonBit0_Test
al 00C361 .DispatchButtonBits
al 00C35E .ExitToInitAlt
al 00C356 .SetBurstWriteIndex
al 00C34A .CheckFrameCounterLimitAlt
al 00C344 .IncrementFrameCounterAlt
al 00C315 .UpdateOamFromButtons
al 00C2FE .CheckAnyButtonHeld
al 00C2F5 .UpdateAllButtonMarkers
al 00C2E4 .PollControllerCountFrames
al 00C2A7 .CopyOamFromTable
al 00C252 .ClearRamPass3Loop
al 00C246 .WriteRamPatternLoop
al 00C23F .InitTestScreen88
al 00C21A .FillOamShadowLoop
al 00C210 .ResetButtonStateAndOam
al 00C1FD .ReadButtonBitUpdateOam
al 00C1F4 .CopyButtonOamLoop
al 00C1EE .PrepareButtonOam
al 00C1CC .UpdateAllOamEntries
al 00C1AD .ScanAllControllerBits
al 00C1A2 .CopySpriteTemplateLoop
al 00C1A0 .CopySpriteTemplateTable
al 00C18F .SampleControllerKeepXAlt
al 00C17F .SampleControllerKeepX
al 00C178 .ClearOamLoop
al 00C174 .ClearOam
al 00C15C .ApplyMarkerValueEnableAudio
al 00C157 .LoadMarkerValue70
al 00C152 .LoadMarkerValueA0
al 00C14D .LoadMarkerValue88
al 00C126 .ExitToInit
al 00C12F .EnterTestMode88
al 00C129 .EnterTestMode70
al 00C10E .RestartInit
al 00C10B .ContinueFrame
al 00C0FF .CheckFrameCounterLimit
al 00C0F9 .IncrementFrameCounter
al 00C111 .DispatchOnMarkerValue
al 00C135 .UpdateButtonMarker
al 00C0E7 .PollControllerShiftRegister
al 00C0DF .MainGameLoop
al 00C05D .ClearRamPass2Loop
al 00C055 .InitTestScreen
al 00C013 .ClearRamPass1Loop
al 00C00A .WaitVblank3
al 00C005 .WaitVblank2
al 00C000 .Reset