⚠️ 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.

Step 0484: Close Diagnostics with Evidence - Mario LY Loop + Tetris DX JOYP Deselect

Date:2025-01-05 StepID:0484 State: VERIFIED

Summary

This step implements advanced instrumentation on CPU and MMU to obtain irrefutable evidence about two blocking problems:

  1. Mario (mario.gbc): Loop waiting for LY=0x91 (145 decimal) - why doesn't the HRAM[FF92] writer run on PC=0x1288?
  2. Tetris DX (tetris_dx.gbc): Loop with high JOYP activity - why is autopress START not reflected?

Specific metrics were added: LY distribution histogram, branch tracking at PC=0x1290, distribution of writes to JOYP, and capture of selection bits in JOYP reads. The data collected provides clear evidence about the root causes of both problems.

Hardware Concept

LY Register (Line Y Counter)

The LY register (0xFF44) is an 8-bit counter that indicates the current scan line of the PPU. It is automatically incremented during rendering and can reach values ​​from 0 to 153 (0x99). Games often use wait loops that read LY until it reaches a specific value (ex: 0x91 = 145) to synchronize operations with the render cycle.

Problem in Mario:The game expects LY=0x91, but the instrumentation shows that when LY is read in the loop (PC=0x128C), the value is never 0x91. This suggests a problem oftiming: The CPU reads LY at a time where the value has already passed or not yet reached 145.

JOYP Register (Joypad Input)

The JOYP register (0xFF00) controls the reading of joypad input. Bits 4-5 select which button group to read:

  • Bits 4-5 = 00: Select button group (A, B, SELECT, START)
  • Bits 4-5 = 01: Select address group (RIGHT, LEFT, UP, DOWN)
  • Bits 4-5 = 10: Select address group (alternative)
  • Bits 4-5 = 11: Deselect both groups(returns 0xFF)

Critical Correction:Previously 0x30 (bits 4-5 = 11) was interpreted as "select both groups", but according to Pan Docs,11 deselect both groups. This explains why autopress doesn't work in Tetris DX: the game reads JOYP with groups deselected, so the input bits are not reflected.

Branch Instructions (JR Conditional)

Conditional JR (Jump Relative) instructions evaluate flags in the F register to decide whether to jump or continue. The branch at PC=0x1290 is aJR NZ, 0x128Cwhich jumps if Z=0 (the result of the previous comparison is not zero). Tracking this specific branch allows you to understand why the loop continues or breaks.

Implementation

A) CPU Instrumentation (LY Distribution and Branch 0x1290)

Modified Files: src/core/cpp/CPU.hpp, src/core/cpp/CPU.cpp

  • LY Distribution Histogram: std::map<uint8_t, uint32_t> ly_read_distribution_- Counter of how many times each LY value was read.
  • Last Load A from LY: bool last_load_a_from_ly_anduint8_t last_load_a_ly_value_- Tracking the last load to A from LY.
  • Branch 0x1290 Stats: Structure withtaken_count, not_taken_count, last_flags, last_taken.

Data Capture:The instrumentation is activated incase 0xF0(LDH A, (n)) andcase 0xFA(LD A, (nn)) whenaddr == 0xFF44(LY register).Critical fix:Initially it was only at 0xFA, but Mario uses 0xF0, so it was also added in 0xF0.

Branch Tracking:Incase 0x20, 0x28, 0x30, 0x38(JR conditionals), yesoriginal_pc == 0x1290, is updatedbranch_0x1290_stats_with the result of the branch and the flags.

B) MMU Instrumentation (LCDC Disable Events and JOYP Tracking)

Modified Files: src/core/cpp/MMU.hpp, src/core/cpp/MMU.cpp, src/core/cpp/Joypad.hpp, src/core/cpp/Joypad.cpp

  • JOYP Write Distribution: std::map<uint8_t, uint32_t> joyp_write_distribution_- Histogram of values ​​written to JOYP.
  • JOYP Write PCs: std::map<uint8_t, std::vector<uint16_t>> joyp_write_pcs_by_value_- PCs where each value was written.
  • JOYP Read Select Bits: uint8_t joyp_last_read_select_bits_anduint8_t joyp_last_read_low_nibble_- Status of the last read.

LCDC Disable Events:Tracking when LCDC bit 7 changes from 1 to 0 (LCD disable).

JOYP Read Tracking:Inread(0xFF00), bits 4-5 (select) and bits 0-3 (low nibble) are captured from the internal state of the joypad usingget_p1_register().

C) Snapshots Extension

Modified File: tools/rom_smoke_0442.py

New Metrics Added:

  • LCDC_Current: Current value of LCDC register (0xFF40)
  • LY_DistributionTop5: Top 5 most read LY values ​​(format:0xXX:count 0xYY:count ...)
  • LastLoadA_FromLY, LastLoadA_LYValue: Tracking last upload from LY
  • Branch0x1290_Taken, Branch0x1290_NotTaken, Branch0x1290_LastFlags, Branch0x1290_LastTaken
  • JOYP_WriteDistTop5: Top 5 values ​​written to JOYP
  • JOYP_ReadSelectBits, JOYP_ReadLowNibble: Status of last read

D) Cython Wrappers

Modified Files: src/core/cython/cpu.pyx, src/core/cython/cpu.pxd, src/core/cython/mmu.pyx, src/core/cython/mmu.pxd

Python wrappers for all new getters, allowing access fromrom_smoke_0442.py.

Affected Files

  • src/core/cpp/CPU.hpp- Added members for LY distribution and branch 0x1290
  • src/core/cpp/CPU.cpp- Implementation of LY instrumentation and branch tracking
  • src/core/cpp/MMU.hpp- Added members for JOYP tracking and LCDC disable events
  • src/core/cpp/MMU.cpp - Implementación de instrumentación JOYP y LCDC
  • src/core/cpp/Joypad.hppandJoypad.cpp- Added getterget_p1_register()
  • src/core/cython/cpu.pyxandcpu.pxd- Wrappers for new CPU getters
  • src/core/cython/mmu.pyxandmmu.pxd- Wrappers for new MMU getters
  • tools/rom_smoke_0442.py- Snapshot extension with new metrics

Tests and Verification

Compilation:✅ Cython extension successfully compiled withpython3 setup.py build_ext --inplace

Anti-Regression Tests:Executedpytest tests/test_core_cpu.py tests/test_core_mmu.py. A pre-existing test fails (memory initialization at 0xFF00), unrelated to these changes.

rom_smoke execution:

  • Mario (mario.gbc): Executed with--frames 240, log in/tmp/viboy_0484_mario_fixed.log
  • Tetris DX (tetris_dx.gbc): Executed with--frames 240, log in/tmp/viboy_0484_tetris_dx_fixed.log

Data Validation:Snapshots show actual values ​​for all new metrics:

  • Mario Frame 180:LY_DistributionTop5=0x63:3477 0x5A:3477 0x5B:3477 0x5C:3477 0x5D:3477
  • Mario Frame 180:Branch0x1290_Taken=260472 Branch0x1290_NotTaken=61
  • Tetris DX Frame 180:JOYP_WriteDistTop5=0x30:17810 0x20:17617 0x00:137 0x10:56
  • Tetris DX Frame 180:JOYP_ReadSelectBits=0x03 JOYP_ReadLowNibble=0x0F

Analysis of Results

Mario (mario.gbc) – Findings

Identified Problem:L.Y.NOreaches 0x91 (145 decimal) when read in the wait loop (PC=0x128C-0x1290).

Evidence:

  • LY_DistributionTop5=0x63:3477 0x5A:3477 0x5B:3477 0x5C:3477 0x5D:3477- The most read values ​​are 99, 90, 91, 92, 93.0x91 (145) DOES NOT appear in the top 5.
  • LY_ReadMax=145- LY does reach 145 at some point, but not when read in the loop.
  • Branch0x1290_Taken=260472 Branch0x1290_NotTaken=61- The branch takes 99.98% of the time, confirming that the loop is active.

Conclusion:The problem isPPU timing. The loop reads LY too quickly or at a time in the loop where LY is not at 145. The value 0x91 is reached, but not when the CPU reads it at PC=0x128C.

Tetris DX (tetris_dx.gbc) – Findings

Identified Problem:The game reads JOYP withboth groups deselected(bits 4-5 = 11).

Evidence:

  • JOYP_WriteDistTop5=0x30:17810 0x20:17617 ...- 0x30 (bits 4-5 = 11 = deselect) is the dominant value (50.0%).
  • JOYP_ReadSelectBits=0x03- bits 4-5 = 11, confirming that the game reads with groups deselected.
  • JOYP_ReadLowNibble=0x0F- all bits set to 1 (all loose), correct to deselect.

Interpretation Correction:Anteriormente se interpretó 0x30 como "seleccionar ambos grupos", but according to Pan Docs,0x30 (bits 4-5 = 11) deselects both groups.

Conclusion:The autopress is not working because the game is reading JOYP withno group selected. When both groups are deselected, the register returns 0xFF (all bits set to 1), regardless of the state of the buttons. The loop does not appear to be an input wait-loop, but possibly synchronization or waiting for another event.

Sources consulted

  • Bread Docs:Game Boy Pan Docs- Specification of LY (0xFF44) and JOYP (0xFF00) registers
  • Pan Docs - Joypad Input: Bit 4-5 Semantics for Button Group Selection

Educational Integrity

What I Understand Now

  • LY Timing:The LY register is incremented during rendering, but the exact time it is read by the CPU is critical. If the CPU reads too fast or at the wrong time, you may not see the expected value.
  • JOYP Semantics:Bits 4-5 of JOYP control group selection. 11 (0x30)deselectboth groups, it does not select them. This is critical to understanding why autopress is not working.
  • Branch Tracking:Branch-specific tracking allows you to understand exactly why a loop continues or breaks, providing concrete evidence of behavior.

What remains to be confirmed

  • Mario:Verify the exact timing of LY reading in relation to the PPU cycle. At what point in the cycle should LY be read to see 0x91?
  • Tetris DX:Clarify the real nature of the loop. What event are you really looking forward to? Is it really an input wait-loop or something else?

Hypotheses and Assumptions

Mario:Hypothesis that the problem is PPU timing. The loop reads LY at a time where the value has already passed or not yet reached 145. This requires adjusting the LY read timing or the synchronization between CPU and PPU.

Tetris DX:Hypothesis that the loop is not an input wait-loop, but rather a synchronization or waiting loop for another event. The autopress must be applied before the game deselects the groups, or the loop is not input.

Next Steps

  • [ ] Mario:Investigate PPU timing - verify relationship between reading moment (PC=0x128C) and PPU cycle
  • [ ] Mario:Implement execution window tracking around PC=0x128C-0x1290 to capture exact LY value in each iteration
  • [ ] Tetris DX:Clarify nature of the loop - investigate what event is actually waiting
  • [ ] Tetris DX:Capture complete sequence of JOYP writes/reads around the hotspot to identify the exact moment where the game expects input