⚠️ Clean-Room / Educational

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

Date:2026-01-05 StepID:0485 State: VERIFIED

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 structures
  • src/core/cpp/CPU.cpp- Tracking in LDH A,(n) and JR NZ, exec coverage for Mario window
  • src/core/cpp/MMU.hpp- JOYPTraceEvent structure (out of class) and counters by selection
  • src/core/cpp/MMU.cpp- JOYP trace tracking in read() and write()
  • src/core/cython/cpu.pxd- Cython declarations for LoopTraceEvent and getters
  • src/core/cython/cpu.pyx- Python implementation of getters and conversion of LoopTraceEvent
  • src/core/cython/mmu.pxd- Cython declarations for JOYPTraceEvent and getters
  • src/core/cython/mmu.pyx- Python implementation of getters and JOYPTraceEvent conversion
  • tools/rom_smoke_0442.py- Capture of Step 0485 metrics in snapshots
  • tests/test_joyp_press_with_selection_0485.py- Test: JOYP press with active selection
  • tests/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

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