Viboy Color - Development Log

Step 0482: Unlock FF92 Route (Mario) + Detect Real Wait-Loop (Tetris DX) + Delete Static State

Summary

This step implements three main objectives:

  1. Phase 0: Hygiene- Delete static state shared between tests
  2. Phase A: Mario- Instrumentation for control flow analysis (Branch Decision Counters, Last Compare/BIT Tracking, LCDC Disable Tracking)
  3. Phase B: Tetris DX- Dynamic wait-loops detector and histogram of unknown opcodes

Implementation

Phase 0: Hygiene - Eliminate Static State

Converted static counters to instance members to isolate tests:

  • MMU: ie_write_count_, last_ie_written_, last_ie_write_pc_, last_ie_write_timestamp_, last_ie_read_value_, ie_read_count_, key1_write_count_, joyp_write_count_, io_read_counts_, lcdc_disable_events_
  • CPU: ei_count_, di_count_

Phase A: Mario - Instrumentation for Analysis

A1) Branch Decision Counters

Gate: VIBOY_DEBUG_BRANCH=1

Implemented inCPU.cpp/CPU.hpp:

  • branch_decisions_: pc map → BranchDecision (taken_count, not_taken_count, last_target, last_taken, last_flags)
  • last_cond_jump_pc_, last_target_, last_taken_, last_flags_
  • Getters exposed in Cython
A2) Last Compare/BIT Tracking

Gate: VIBOY_DEBUG_BRANCH=1

Implemented inCPU.cpp/CPU.hpp:

  • Last Compare: last_cmp_pc_, last_cmp_a_, last_cmp_imm_, last_cmp_result_flags_
  • Last BIT: last_bit_pc_, last_bit_n_, last_bit_value_
  • Tracking in instructionsCP n(0xFE) andBIT n, r(CB opcodes)
  • Getters exposed in Cython
A3) LCDC Disable Tracking

Implemented inMMU.cpp/MMU.hppandPPU.cpp/PPU.hpp:

  • lcdc_disable_events_: event counter (1→0)
  • last_lcdc_write_pc_, last_lcdc_write_value_
  • PPU::handle_lcd_disable(): reset LY to 0, STAT mode to HBlank
  • Getters exposed in Cython
A4) Test Clean-Room LCDC Disable

Archive: tests/test_lcdc_disable_resets_ly_0482.py

Result:HAPPENS

Verify that when LCDC bit7 goes from 1→0:

  • LY resets to 0 and stabilizes
  • STAT mode is set to HBlank (mode 0)
  • lcdc_disable_events == 1

Phase B: Tetris DX - Detect Real Wait-Loop

B1) Dynamic Wait-Loop Detector

Archive: tools/rom_smoke_0442.py

Functiondetect_dynamic_wait_loop():

  • Analyze I/O reads program in hotspot
  • Identifies I/O read dominant
  • Correlates withlast_cmp/last_bitto determine waiting condition
  • Returns:waits_on_addr, mask, cmp, bit, io_reads_distribution
B2) Unknown Opcode Histogram

Archive: tools/rom_smoke_0442.py

Functionget_unknown_opcode_histogram():

  • Count unknown opcodes (DB) in disasm window
  • Returns top 10 opcodes sorted by frequency
  • Allows you to prioritize implementation of opcodes in hotspots
Integration in Snapshots

Added to snapshotsrom_smoke_0442.py:

  • BranchInfo: branch counters information (if VIBOY_DEBUG_BRANCH=1)
  • LastCmp: last CP executed (PC, A, Imm)
  • LastBit: last BIT executed (PC, Bit, Val)
  • LCDC_DisableEvents: disable event counter
  • DynamicWaitLoop: dynamic detector result
  • UnknownOpcodes: top 5 unknown opcodes in hotspot

Hardware Concept

LCDC (LCD Control Register - 0xFF40)

The LCDC register controls the state of the LCD. Bit 7 (LCDC.7) turns the LCD on/off. When the LCD turns off (bit 7 goes from 1→0), according to Pan Docs:

  • LY (Line Y) is reset to 0
  • STAT mode is set to a stable state (Mode 0 = HBlank)
  • There should not be infinite frame pending

This behavior is critical for many games that turn off the LCD during transitions screen or initialization.

Branch Decision Tracking

Branch decision tracking allows you to analyze the control flow of the program:

  • Taken/Not-Taken Counts: How many times a conditional jump was taken vs not taken
  • Last Target/Flags: Last target and flags at the time of the jump
  • Useful for identifying loops and conditions that block progress

Last Compare/BIT Tracking

The tracking of last comparisons and bit tests allows correlating:

  • CP (Compare): Last value compared (A vs immediate)
  • BIT: Last bit tested (bit number and value)
  • With I/O reads to identify wait conditions in wait-loops

Tests and Verification

LCDC Test Disable

Command:

pytest tests/test_lcdc_disable_resets_ly_0482.py -v

Result:

1 passed in 0.25s

Test code (key fragment):

def test_lcdc_disable_resets_ly(self):
    mmu = PyMMU()
    ppu = PyPPU(mmu)
    mmu.set_ppu(ppu)
    
    # Turn on LCD
    mmu.write(IO_LCDC, 0x91)
    ppu.step(4560) # 10 scanlines
    assert ppu.get_ly() == 10
    
    # Turn off LCD
    mmu.write(IO_LCDC, 0x11)
    ppu.step(1000)
    
    # Verify LY reset
    assert ppu.get_ly() == 0
    assert ppu.get_mode() == 0 # HBlank
    assert mmu.get_lcdc_disable_events() == 1

Validation:Test validates compiled C++ module (PPU::handle_lcd_disable)

Running rom_smoke

Run ROMs:

  • mario.gbc- 240 frames
  • tetris_dx.gbc- 240 frames
  • tetris.gb- 240 frames

Logs:

  • /tmp/mario_smoke_0482.log
  • /tmp/tetris_dx_smoke_0482.log
  • /tmp/tetris_smoke_0482.log

Affected Files

  • src/core/cpp/MMU.hpp / MMU.cpp
  • src/core/cpp/CPU.hpp / CPU.cpp
  • src/core/cpp/PPU.hpp / PPU.cpp
  • src/core/cython/mmu.pxd / mmu.pyx
  • src/core/cython/cpu.pxd / cpu.pyx
  • tests/test_lcdc_disable_resets_ly_0482.py(new)
  • tools/rom_smoke_0442.py

References

  • Pan Docs - LCD Control Register (FF40 - LCDC)
  • Pan Docs - CPU Instruction Set (CP, BIT)
  • Pan Docs - LCD Status Register (STAT)