⚠️ 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 LDH a8≥0x80 + Tetris DX JOYP Internal State

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

Summary

Step 0486 implementa instrumentación quirúrgica para recopilar evidencia dura sobre dos issues bloqueantes:

  • Mario (mario.gbc):Investigate potential addressing bugLDH a8whena8 >= 0x80that could preventHRAM[FF92]is written and read correctly inI.E..
  • Tetris DX (tetris_dx.gbc):Investigate behavior ofJOYP, specifically if the ROM reads with active selection or if the emulator is incorrectly handling writes/reads ofJOYP.

Detailed tracking gated by environment variables is implemented, clean-room tests to validateLDH, y intelligent autopress basado en eventos para Tetris DX.

Hardware Concept

LDH (Load High) Instruction

The instructionsLDH A,(a8)(opcode 0xF0) andLDH (a8),A(opcode 0xE0) access to the high memory I/O region (0xFF00-0xFF7F) using an 8-bit operand.

The effective address is calculated as0xFF00 | a8(OR bitwise), not as addition with sign-extension. This means that whena8 >= 0x80, the effective address is0xFF80or higher, accessing HRAM (High RAM) instead of I/O registers.

Example: LDH (0x92),Awrite in0xFF92(HRAM), not in0x0092neither0xFE92.

Fountain:Pan Docs - CPU Instruction Set

HRAM (High RAM) - 0xFF80-0xFFFE

HRAM is a 127-byte region of high-speed RAM accessible only by the CPU. It is frequently used for temporary variables and flags that require quick access.

0xFF92is a specific address in HRAM that Mario uses to temporarily store the value ofI.E.before writing it back.

JOYP Register (0xFF00) - Internal State

The JOYP register has select bits (4-5) that determine which button group is read. When a value is written, the select bits are stored internally and affect subsequent reads.

Potential problem:If the emulator does not properly preserve the internal selection state between writes and reads, or if there is a delay between write and read, the ROM could read with incorrect selection.

Fountain:Pan Docs - Joypad Input

Implementation

Phase A: Mario - LDH Effective Address + Real HRAM Verification

A1) LDH Surgical Instrumentation in CPU

was addedLDHAddressWatchstruct inCPU.hppwhat tracks:

  • PC of the last executed LDH instruction
  • Operand a8 of the instruction
  • Calculated effective address
  • Operation type (read/write)
  • Addressing discrepancy counter

Gated byVIBOY_DEBUG_MARIO_FF92=1.

A2) Specific Tracking HRAM FF92 in MMU

was addedHRAMFF92Watchstruct inMMU.hppwhat tracks:

  • PC and value of last write to 0xFF92
  • PC and value of the last read of 0xFF92
  • Immediate readback after write (diagnostic)
  • Write/readback discrepancy counter

Phase B: Mario - Complete Chain FF92 → IE

B1) Trace Mini FF92→IE

was addedFF92ToIETracestruct (global scope) that detects the sequence:

  1. Write to FF92 on PC=0x1288
  2. Read from FF92 on PC=0x1298
  3. Write to IE on PC=0x129A

Captures written/read values ​​and occurrence frame.

B2) IE/IME/IF in Snapshots

Added explicit fields to snapshots inrom_smoke_0442.py: IE_value, IE_last_write_pc, IE_last_write_val, IME_value, IF_value, irq_serviced_count.

Phase C: Clean-Room Tests

was createdtests/test_ldh_a8_ge_0x80_0486.pywith 4 tests that validate:

  • LDH (0x92),Awrite in0xFF92
  • LDH A,(0x92)read from0xFF92
  • LDH (0xFF),Awrite in0xFFFF(IE)
  • LDH A,(0xFF)read from0xFFFF(IE)

Result:✅ All tests pass.

Phase D: Tetris DX - JOYP Trace with Internal State

D1) Update JOYPTraceEvent

It was updatedJOYPTraceEventwith:

  • Sourceenum:PROGRAMeitherCPU_POLL
  • p1_reg_before, p1_reg_after, p1_reg_at_read
  • select_bits_at_read, low_nibble_at_read

D2) JOYP Counters by Source and Selection

Added 6 counters that distinguish between reads from program vs cpu_poll, and between buttons selected, dpad selected, or none selected.

Phase E: Intelligent Autopress

Implemented event-based autopress insrc/viboy.pythat:

  • Activates START when it detects write to JOYP with buttons selected (bit 5 = 0)
  • Libera START después de read con buttons selected o timeout de 60 frames

Tests and Verification

Clean-Room LDH Tests

Command: pytest tests/test_ldh_a8_ge_0x80_0486.py -v

Result:

tests/test_ldh_a8_ge_0x80_0486.py::TestLDHA8Ge0x80::test_ldh_write_0x92_writes_to_ff92 PASSED
tests/test_ldh_a8_ge_0x80_0486.py::TestLDHA8Ge0x80::test_ldh_read_0x92_reads_from_ff92 PASSED
tests/test_ldh_a8_ge_0x80_0486.py::TestLDHA8Ge0x80::test_ldh_write_0xFF_writes_to_ffff PASSED
tests/test_ldh_a8_ge_0x80_0486.py::TestLDHA8Ge0x80::test_ldh_read_0xFF_reads_from_ffff PASSED

============================== 4 passed in 0.14s ==============================

Validation:The tests confirm thatLDHcorrectly calculates the effective direction whena8 >= 0x80. There is no addressing bug in the base implementation.

Results

Mario (mario.gbc) - 300 frames

Evidence from writes to FF92:✅ Logs show[HRAM-WRITE] Write FF92=00 PC:1288

Structured tracking: ⚠️ HRAM_FF92_WriteCount=0in snapshots (possible issue with gating or counter)

IE value:⚠️ Stay in0x00throughout the execution

Conclusion:There is no evidence of a routing bug inLDH. The problem seems to be in the FF92→IE chain, whereI.E.is not updating correctly after writing to FF92.

Tetris DX (tetris_dx.gbc) - 300 frames

JOYP writes:✅ ROM writes0x30(buttons selected) frequently (17811 writes in 240 frames)

JOYP reads:⚠️ Reads showselect_bits=0x03(no selection active) andlow_nibble=0x0F(all buttons "released")

Conclusion:The ROM is writing to select buttons, but reads occur when the selection has already been deactivated. This suggests a timing or preservation problem. selection state between write and read.

Affected Files

  • src/core/cpp/CPU.hpp- AddedLDHAddressWatchstruct and getters
  • src/core/cpp/CPU.cpp- LDH tracking in opcodes 0xE0 and 0xF0
  • src/core/cpp/MMU.hpp- AddedHRAMFF92Watch, FF92ToIETrace, updatedJOYPTraceEvent
  • src/core/cpp/MMU.cpp- Implementation of FF92, IE, and JOYP tracking
  • src/core/cython/cpu.pyx, cpu.pxd- Cython wrappers for LDH getters
  • src/core/cython/mmu.pyx, mmu.pxd- Cython wrappers for FF92, IE, JOYP getters
  • tests/test_ldh_a8_ge_0x80_0486.py- Clean-room tests for LDH
  • tools/rom_smoke_0442.py- Added IE/IME/IF fields to snapshots
  • src/viboy.py- Implemented intelligent autopress

Next Steps

  1. Mario:Investigate whyHRAM_FF92_WriteCountremains at 0 despite the logs. Check gatingVIBOY_DEBUG_MARIO_FF92=1.
  2. Mario:Investigate whyI.E.It doesn't update after writing to FF92. Check FF92→IE sequence usingget_ff92_to_ie_trace().
  3. Tetris DX: Ejecutar con intelligent autopress activo para recopilar evidencia de reads con START bit = 0.
  4. Tetris DX:Investigate timing between writes and reads of JOYP. Check if there is a delay between write and read that causes the selection to be disabled.