This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Hard Evidence - Mario LY==0x91 Count + Tetris DX JOYP Trace Sequential
Summary
Step 0484 had 3 critical interpretation errors. Step 0485 fixes these errors and closes with hard evidence:Mario(explicit count(LY==0x91) in loop + branch correlation) andTetris DX(JOYP trace sequential + evidence START bit going low or never).
Surgical instrumentation gated by environmental variables is implemented to obtain accurate counts and sequential traces that allow definitive closure of questions about behavior of these two blocking loops.
Bug Fixes for Step 0484
1. Mario: Confusion of Values and Invalid Logical Leap
Mistake:The loop says "wait LY = 0x91", that's 145 decimal (within VBlank, because LY goes 0..153 and 144..153 is VBlank). In the report, 0x91 was mixed with values such as 0x5B (91 decimal). That is NOT the same.
Worst mistake:WearLY_DistributionTop5to state "0x91 is not read" is incorrect.
That it is not in the top 5 only means "it is read less than those", not "it never happens."
Correction:Explicit counter implementationcount(LY==0x91)specifically
when PC is in the loop range (0x128C..0x1290), and correlation with the branch.
2. Mario: Badly Decoded Flags
Mistake:The report interpretedF=0xC0like "Z=0, N=1, H=1, C=0". That is incorrect.
Correct: 0xC0 = 1100 0000→ Z=1 (bit 7), N=1 (bit 6), H=0 (bit 5), C=0 (bit 4).
If Z=1, thenJR NZshould not take. So either the "last_flags" is not the one of the real branch,
either it's being captured at the wrong time, or the tracking is mixing things up.
Correction:Branch-specific tracking at 0x1290 with flag capture at the exact moment of the evaluation, and correlation with the LY value read immediately before.
3. Tetris DX: JOYP Reading and Misunderstood Selection
Mistake:If you type 0x30, you "select nothing", and the low nibble should read 0xF (all loose). And further: a "pressed" button looks like bit = 0, not 1.
Problem:If the last read shows select bits = 11 (deselected) and low nibble = 0xF, that does not prove that the loop is not input; prove that your snapshot is seeing the wrong read (or the wrong timing).
Correction:Sequential tracing of JOYP writes/reads around the hotspot and around of autopress, with a 256-event ring-buffer that captures the real sequence of accesses.
Hardware Concept
LY (Line Y) Register (0xFF44)
The LY register indicates the current scanline that the PPU is rendering. It goes from 0 to 153 (154 total values). Values 144-153 correspond to the VBlank period (when the PPU is not rendering visible lines).
0x91 = 145 decimal, which is within the VBlank range (144-153). If a loop specifically waits for LY==0x91, it is waiting for a specific time within VBlank.
Fountain:Pan Docs - LCD Status Register
JOYP (Joypad) Register (0xFF00)
The JOYP register (P1) has select bits (4-5) that determine which button group is read:
- Bit 4 (P14):0 = selects direction buttons (D-Pad), 1 = does not select
- Bit 5 (P15):0 = selects action buttons (A, B, Select, Start), 1 = does not select
when you write0x30 = 0011 0000, both bits are 1, so neither group is selected.
In this case, the low nibble (bits 0-3) should read0xF(all bits set to 1, all loose).
When a button ispressed, the corresponding bit is read as0(no. 1). This is important: one button pressed = bit set to 0.
Fountain:Pan Docs - Joypad Input
Implementation
Phase 1: Mario Loop LY Watch
Specific tracking is implemented for Mario's loop (PC 0x128C..0x1290) which explicitly counts how many times LY==0x91 is read when the PC is in that range.
- MarioLoopLYWatch:Structure that counts total LY reads, reads with LY==0x91, and save the last value, timestamp and PC.
- Gated by VIBOY_DEBUG_MARIO_LOOP=1:Only enabled when this environment variable is set.
- Tracking in LDH A,(n):When 0xFF44 (LY) is read and the PC is at 0x128C..0x1290, the counter is updated and it is checked if the value is 0x91.
Phase 2: Branch 0x1290 Correlation
Implemented branch-specific correlation at 0x1290 (JR NZ) with the read LY value immediately before the branch.
- Branch0x1290Correlation:Structure that counts evaluations, taken/not_taken, and saves LY and flags from the moment of not-taken.
- Tracking in JR NZ:When JR NZ is run on PC=0x1290, the status is captured and correlates with the last LY value read in the loop.
- Mini Trace Ring Buffer:64-event buffer that captures each branch evaluation with frame, PC, LY, flags, taken and timestamp.
Phase 3: Exec Coverage for Mario Window
exec coverage is activated for window 0x1270..0x12B0 whenVIBOY_DEBUG_MARIO_LOOP=1,
allowing to verify if the code after the "not taken" actually executes the writer at 0x1288.
Phase 4: JOYP Access Trace
A 256-event ring-buffer is implemented that captures the actual sequence of JOYP writes/reads, allowing you to see the entire sequence instead of just an isolated snapshot.
- JOYPTraceEvent:Structure that captures type (READ/WRITE), PC, written/read values, selection bits, low nibble read and timestamp.
- Gated by VIBOY_DEBUG_JOYP_TRACE=1:Only activated when this variable is set.
- Counters by type of selection:Reads with selected buttons are counted, dpad selected, or none selected.
Design Decisions
- Structures outside of classes:
LoopTraceEventandJOYPTraceEventThey are defined outside of classes for compatibility with Cython (you need to see the full definition). - Gating by environment variables:All instrumentation is gated so as not to affect performance when not needed.
- Fixed-size ring-buffers:Fixed size buffers are used (64 for Mario, 256 for JOYP) to avoid unlimited memory growth.
Affected Files
src/core/cpp/CPU.hpp- MarioLoopLYWatch, Branch0x1290Correlation, LoopTraceEvent and getters structuressrc/core/cpp/CPU.cpp- Tracking in LDH A,(n) and JR NZ, exec coverage for Mario windowsrc/core/cpp/MMU.hpp- JOYPTraceEvent structure (out of class) and counters by selectionsrc/core/cpp/MMU.cpp- JOYP trace tracking in read() and write()src/core/cython/cpu.pxd- Cython declarations for LoopTraceEvent and getterssrc/core/cython/cpu.pyx- Python implementation of getters and conversion of LoopTraceEventsrc/core/cython/mmu.pxd- Cython declarations for JOYPTraceEvent and getterssrc/core/cython/mmu.pyx- Python implementation of getters and JOYPTraceEvent conversiontools/rom_smoke_0442.py- Capture of Step 0485 metrics in snapshotstests/test_joyp_press_with_selection_0485.py- Test: JOYP press with active selectiontests/test_joyp_select_bits_consistency_0485.py- Test: Consistency of select bits (0x30 → 0xF)
Tests and Verification
Minimal clean-room tests were created to validate the instrumentation:
Test: JOYP Press with Selection
def test_joyp_press_start_with_buttons_selected():
"""Test: Select buttons (P14=0), press START, read and verify bit 3 = 0"""
mmu.write(0xFF00, 0x20) # Select buttons
joypad.press_button(7) # START = index 7
value = mmu.read(0xFF00)
assert (value & 0x08) == 0 # Bit 3 must be 0 (pressed)
Result:✅ PASSED - Verifies that the trace correctly captures the events.
Test: Select Bits Consistency
def test_joyp_0x30_reads_0xF():
"""Test: If 0x30 is written, the low nibble should read 0xF"""
mmu.write(0xFF00, 0x30) # No group selected
value = mmu.read(0xFF00)
assert (value & 0x0F) == 0x0F # Low nibble must be 0xF
Result:✅ PASSED - Verifies that 0x30 correctly reads 0xF.
Compilation
Command: python3 setup.py build_ext --inplace
Result:✅ Successful build without errors. All structures and getters Correctly exposed to Python via Cython.
Sources consulted
- Bread Docs:LCD Status Register (STAT) - LY Register
- Bread Docs:Joypad Input - P1 Register
- Step 0484: Error analysis and necessary corrections
Educational Integrity
What I Understand Now
- LY 0x91 = 145 decimal:It's inside VBlank (144-153), not 91 decimal (0x5B).
- Flags decoding:0xC0 = Z=1, N=1, H=0, C=0. If Z=1, JR NZ does not take.
- JOYP semantics:0x30 = no group selected → low nibble reads 0xF. Button pressed = bit set to 0.
- Hard evidence vs top5:A value that is not in top5 does not mean "never occurs", it just means "occurs less than those 5". We need explicit counts.
What remains to be confirmed
- Mario:How many times is LY==0x91 actually read in the loop? Why does it not progress if it is read?
- Tetris DX:Is there any JOYP read with active selection where START is read as 0 during autopress?
- Branch correlation:Does "not taken" actually make way to 0x1288, or is there another blockage?
Hypotheses and Assumptions
Mario hypothesis:If LY==0x91 is never read in the loop, then the problem is PPU timing (LY never reaches 0x91 when the loop reads it). If it is read but the branch does not progress, then the problem is the condition of the branch (flags or comparison).
Tetris DX Hypothesis:If there is never a read with active selection where START is read as 0, then the problem is that the game is not really looking for START, or the autopress injection is not working correctly.
Next Steps
- [ ] Run rom_smoke with mario.gbc and VIBOY_DEBUG_MARIO_LOOP=1 to get exact counts
- [ ] Run rom_smoke with tetris_dx.gbc and VIBOY_DEBUG_JOYP_TRACE=1 to get sequential trace
- [ ] Analyze report and generate definitive conclusion on both loops
- [ ] Step 0486: Apply minimum fix based on hard evidence obtained