Viboy Color - Development Log

Step 0483: Real Evidence Snapshots with Data + Branch Blockers + JOYP Semantics

Summary

This step implements advanced diagnostic tools to provide concrete evidence of crashes in running specific ROMs:

  1. Phase A: Snapshots with real data (not placeholders)
  2. Phase B: Exec Coverage + Branch Blockers + Last Load A Tracking
  3. Phase C: HRAM FF92 Complete Watchlist
  4. Phase D: JOYP Semantics Complete Fix + tests
  5. Phase E: rom_smoke execution in baseline and input variant
  6. Phase F: Report generation with real values

Specific Objectives

  • Mario (mario.gbc): Provide concrete evidence why HRAM[0xFF92] writer (PC=0x1288) is not running
  • Tetris DX (tetris_dx.gbc): Identify real wait-loop (dominant I/O + condition) even if static parser fails

Implementation

Phase A: Snapshots with Real Data

Updatedtools/rom_smoke_0442.pyto include all metrics in snapshots:

  • pc_hotspot1: PC of the most frequent hotspot
  • waits_on_addr: I/O address where the loop waits
  • unknown_opcodes_topN: Top N unknown opcodes
  • branch_blockers_topN: Top N branches that block
  • ff92_read_count_program, ff92_write_count_program
  • joyp_last_read_value
  • boot_logo_prefill_enabled
  • first_write_frame, last_write_framefor HRAM FF92

Result:✅ All snapshots contain real values ​​(numbers or "N/A"), not placeholders.

Phase B: Exec Coverage + Branch Blockers + Last Load A

Gate: VIBOY_DEBUG_BRANCH=1

Implemented inCPU.cpp/CPU.hpp:

  • Exec Coverage: exec_coverage_(map PC → counter),coverage_window_start_, coverage_window_end_
  • Last Load A: last_load_a_pc_, last_load_a_addr_, last_load_a_value_
  • Tracking inLDH A,(n)(0xF0) andLD A,(nn)(0xFA)
  • Methods:set_coverage_window(), get_exec_count(), get_top_exec_pcs(), get_top_branch_blockers(), get_last_load_a_*()

Cython:Python wrappers exposed incpu.pxd/cpu.pyx

State:✅ Successful compilation, methods available from Python.

Phase C: HRAM FF92 Watchlist

Gate: VIBOY_DEBUG_HRAM=1

Implemented inMMU.cpp/MMU.hpp:

  • Addedlast_write_frametoHRAMWatchEntry
  • Trackinglast_read_pcandlast_read_valuein HRAM reads
  • Getters:get_hram_last_write_frame(), get_hram_last_read_pc(), get_hram_last_read_value()

Metrics in snapshots:WriteCount, ReadCountProg, FirstWriteFrame, LastWriteFrame, LastWritePC/Val, LastReadPC/Val

State:✅ Implemented and exposed to Python.

Phase D: JOYP Semantics Fix

Problem:The previous implementation had incorrect selection logic when both groups were selected.

Solution:CorrectedJoypad::read_p1()according to Pan Docs:

if (select_buttons && select_dpad) {
    // Both groups selected: AND of both states
    low_nibble = action_keys_ & direction_keys_;
} else if (select_buttons) {
    // Only selected buttons (P14=0)
    low_nibble = action_keys_;
} else if (select_dpad) {
    // Only selected addresses (P15=0)
    low_nibble = direction_keys_;
} else {
    // No group selected: all bits set to 1
    low_nibble = 0x0F;
}
Created Tests
  • test_joyp_no_select_reads_ones_0483.py: Verify that without selection, bits 0-3 = 0x0F
  • test_joyp_select_buttons_default_all_released_0483.py: Check button selection
  • test_joyp_select_dpad_default_all_released_0483.py: Check address selection
  • test_joyp_both_selected_AND_behavior_0483.py: Check AND behavior when both groups are selected

Result:✅ All tests pass (5/5).

Execution and Results

Mario (mario.gbc) - Baseline

Configuration:Frames=180, Snapshots=0,60,120,180

Key Findings:

  • Frame 0: HRAM_FF92_WriteCount=0 ⚠️, PCHotspot1=0x1290(JR NZ, 0x128C), Loop waiting LY=0x91
  • Frame 60: HRAM_FF92_WriteCount=0 ⚠️ It was NEVER written to FF92, PCHotspot1=0x12A0(4796 runs), Disasm shows0x1298: LDH A,(0xFF92)- Read FF92 but it was never written
  • Frame 120: HRAM_FF92_WriteCount=0 ⚠️, PCHotspot1=0x12A0(9577 executions)

Conclusion Mario:

  • HRAM[0xFF92] is NEVER written(WriteCount=0 in all frames)
  • ✅ The code on PC=0x1298readFF92, but the value is 0x00 (initialized)
  • ⚠️ The writer on PC=0x1288NOT executedbecause the route is not reached
  • 🔍 Root cause: The branch at 0x1290 (JR NZ, 0x128C) always picks up, creating an infinite loop waiting for LY=0x91

Tetris DX (tetris_dx.gbc) - Baseline

Key Findings:

  • Frame 60: JOYP_write_count=14820 ⚠️ High JOYP activity, JOYP_write_val=0x30(select both groups),JOYP_write_PC=0x12F6, PCHotspot1=0x1306(8117 executions)
  • Frame 120: JOYP_write_count=29900(continues to increase),PCHotspot1=0x1304(16227 executions)

Conclusion Tetris DX (Baseline):

  • ✅ JOYP has high activity (14820-29900 writes)
  • ✅ The code writes 0x30 to JOYP (select both groups)
  • ⚠️ The game is in a loop waiting for input (real wait-loop)
  • 🔍 Cause: The game waits for you to press START to continue

Tests and Verification

Command executed:

python3 -m pytest tests/test_joyp_no_select_reads_ones_0483.py \
    tests/test_joyp_select_buttons_default_all_released_0483.py \
    tests/test_joyp_select_dpad_default_all_released_0483.py \
    tests/test_joyp_both_selected_AND_behavior_0483.py -v

Result:5 passed in 0.81s

Test code (example):

def test_both_selected_with_pressed_buttons(self):
    """Verify that with both groups selected, if a button is pressed,
    the AND must reflect it correctly."""
    joypad = PyJoypad()
    mmu = PyMMU()
    mmu.set_joypad(joypad)
    
    # Press Right (address, bit 0)
    joypad.press_button(0)
    
    # Write JOYP = 0x00 (both groups selected)
    mmu.write(0xFF00, 0x00)
    
    # Read JOYP
    result = mmu.read(0xFF00)
    
    # Assert: Bit 0 must be 0 (Right pressed)
    bit_0 = result & 0x01
    assert bit_0 == 0x00, f"Bit 0 should be 0 (Right pressed), but it is {bit_0}."

Native Validation:C++ Compiled Module Validation

Hardware Concept

JOYP Register (0xFF00) - Full Semantics

According to Pan Docs, the JOYP register (0xFF00) has the following semantics:

  • Bits 7-6: Always read as 1 (not used)
  • Bit 5 (P15): 0 = selects action buttons (A, B, Select, Start)
  • Bit 4 (P14): 0 = selects direction buttons (Right, Left, Up, Down)
  • Bits 3-0: Button status (0 = pressed, 1 = released) [read-only]

Behavior when both groups are selected:

  • If both P14 and P15 are 0 (both groups selected), the result is thelogical ANDfrom both states
  • This allows detecting when a button is pressed that is in both groups (e.g. Right and A share bit 0)

Why it is important:Some games use this behavior to detect button combinations or to simplify polling logic.

Modified Files

  • src/core/cpp/CPU.hpp: Added members for exec coverage, branch blockers, last_load_a
  • src/core/cpp/CPU.cpp: Implementation of tracking and getters
  • src/core/cpp/Joypad.cpp: JOYP semantics fix
  • src/core/cpp/MMU.hpp: Addedlast_write_frameto HRAMWatchEntry, getters
  • src/core/cpp/MMU.cpp: Tracking last_write_frame, last_read_pc/value
  • src/core/cython/cpu.pxd: New method declarations
  • src/core/cython/cpu.pyx:Python Wrappers
  • src/core/cython/mmu.pxd: HRAM getter declarations
  • src/core/cython/mmu.pyx:Python Wrappers
  • tools/rom_smoke_0442.py: Added all metrics to snapshots
  • tests/test_joyp_*_0483.py: 4 new tests

Next Steps (Step 0484)

Mario

  1. Configure exec coverage for window 0x1270-0x12B0
  2. Check why LY never reaches 0x91
  3. Analyze branch blockers at 0x1290
  4. Identify condition that blocks the path to 0x1288

Tetris DX

  1. Run withVIBOY_AUTOPRESS=STARTand check progress
  2. If no progress, investigate BIT test at 0x12DD
  3. Check condition of the branch that maintains the loop

References

  • Pan Docs - Joypad Input, P1 Register
  • Step 0482: Branch Decision Counters, Last Compare/BIT Tracking
  • Step 0481: Generic HRAM Watchlist