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
Summary
Step 0486 implementa instrumentación quirúrgica para recopilar evidencia dura sobre dos issues bloqueantes:
- Mario (mario.gbc):Investigate potential addressing bug
LDH a8whena8 >= 0x80that could preventHRAM[FF92]is written and read correctly inI.E.. - Tetris DX (tetris_dx.gbc):Investigate behavior of
JOYP, 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:
- Write to FF92 on PC=0x1288
- Read from FF92 on PC=0x1298
- 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 in0xFF92LDH A,(0x92)read from0xFF92LDH (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_POLLp1_reg_before,p1_reg_after,p1_reg_at_readselect_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 getterssrc/core/cpp/CPU.cpp- LDH tracking in opcodes 0xE0 and 0xF0src/core/cpp/MMU.hpp- AddedHRAMFF92Watch,FF92ToIETrace, updatedJOYPTraceEventsrc/core/cpp/MMU.cpp- Implementation of FF92, IE, and JOYP trackingsrc/core/cython/cpu.pyx,cpu.pxd- Cython wrappers for LDH getterssrc/core/cython/mmu.pyx,mmu.pxd- Cython wrappers for FF92, IE, JOYP getterstests/test_ldh_a8_ge_0x80_0486.py- Clean-room tests for LDHtools/rom_smoke_0442.py- Added IE/IME/IF fields to snapshotssrc/viboy.py- Implemented intelligent autopress
Next Steps
- Mario:Investigate why
HRAM_FF92_WriteCountremains at 0 despite the logs. Check gatingVIBOY_DEBUG_MARIO_FF92=1. - Mario:Investigate why
I.E.It doesn't update after writing to FF92. Check FF92→IE sequence usingget_ff92_to_ie_trace(). - Tetris DX: Ejecutar con intelligent autopress activo para recopilar evidencia de reads con START bit = 0.
- 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.