⚠️ Clean-Room / Educativo

Este proyecto es educativo y Open Source. No se copia código de otros emuladores. Implementación basada únicamente en documentación técnica y tests permitidas.

Evidencia Dura Mario LDH a8≥0x80 + Tetris DX JOYP Estado Interno

Fecha: 2026-01-05 Step ID: 0486 Estado: VERIFIED

Resumen

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

  • Mario (mario.gbc): Investigar potencial bug de direccionamiento LDH a8 cuando a8 >= 0x80 que podría impedir que HRAM[FF92] se escriba y lea correctamente en IE.
  • Tetris DX (tetris_dx.gbc): Investigar comportamiento de JOYP, específicamente si la ROM lee con selección activa o si el emulador está manejando incorrectamente writes/reads de JOYP.

Se implementa tracking detallado gated por variables de entorno, tests clean-room para validar LDH, y intelligent autopress basado en eventos para Tetris DX.

Concepto de Hardware

LDH (Load High) Instruction

Las instrucciones LDH A,(a8) (opcode 0xF0) y LDH (a8),A (opcode 0xE0) acceden a la región de I/O de alta memoria (0xFF00-0xFF7F) usando un operando de 8 bits.

La dirección efectiva se calcula como 0xFF00 | a8 (OR bitwise), no como suma con sign-extension. Esto significa que cuando a8 >= 0x80, la dirección efectiva es 0xFF80 o superior, accediendo a HRAM (High RAM) en lugar de I/O registers.

Ejemplo: LDH (0x92),A escribe en 0xFF92 (HRAM), no en 0x0092 ni 0xFE92.

Fuente: Pan Docs - CPU Instruction Set

HRAM (High RAM) - 0xFF80-0xFFFE

HRAM es una región de 127 bytes de RAM de alta velocidad accesible solo por la CPU. Se usa frecuentemente para variables temporales y flags que requieren acceso rápido.

0xFF92 es una dirección específica en HRAM que Mario usa para almacenar temporalmente el valor de IE antes de escribirlo de vuelta.

JOYP Register (0xFF00) - Estado Interno

El registro JOYP tiene bits de selección (4-5) que determinan qué grupo de botones se lee. Cuando se escribe un valor, los bits de selección se almacenan internamente y afectan las lecturas subsecuentes.

Problema potencial: Si el emulador no preserva correctamente el estado interno de selección entre writes y reads, o si hay un delay entre write y read, la ROM podría leer con selección incorrecta.

Fuente: Pan Docs - Joypad Input

Implementación

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

A1) Instrumentación Quirúrgica LDH en CPU

Se añadió LDHAddressWatch struct en CPU.hpp que trackea:

  • PC de la última instrucción LDH ejecutada
  • Operando a8 de la instrucción
  • Dirección efectiva calculada
  • Tipo de operación (read/write)
  • Contador de discrepancias de direccionamiento

Gated por VIBOY_DEBUG_MARIO_FF92=1.

A2) Tracking Específico HRAM FF92 en MMU

Se añadió HRAMFF92Watch struct en MMU.hpp que trackea:

  • PC y valor del último write a 0xFF92
  • PC y valor del último read de 0xFF92
  • Readback inmediato después de write (diagnóstico)
  • Contador de discrepancias write/readback

Fase B: Mario - Complete Chain FF92 → IE

B1) Trace Mini FF92→IE

Se añadió FF92ToIETrace struct (global scope) que detecta la secuencia:

  1. Write a FF92 en PC=0x1288
  2. Read de FF92 en PC=0x1298
  3. Write a IE en PC=0x129A

Captura valores escritos/leídos y frame de ocurrencia.

B2) IE/IME/IF en Snapshots

Se añadieron campos explícitos a snapshots en rom_smoke_0442.py: IE_value, IE_last_write_pc, IE_last_write_val, IME_value, IF_value, irq_serviced_count.

Fase C: Clean-Room Tests

Se creó tests/test_ldh_a8_ge_0x80_0486.py con 4 tests que validan:

  • LDH (0x92),A escribe en 0xFF92
  • LDH A,(0x92) lee de 0xFF92
  • LDH (0xFF),A escribe en 0xFFFF (IE)
  • LDH A,(0xFF) lee de 0xFFFF (IE)

Resultado: ✅ Todos los tests pasan.

Fase D: Tetris DX - JOYP Trace con Estado Interno

D1) Actualización JOYPTraceEvent

Se actualizó JOYPTraceEvent con:

  • Source enum: PROGRAM o CPU_POLL
  • p1_reg_before, p1_reg_after, p1_reg_at_read
  • select_bits_at_read, low_nibble_at_read

D2) Contadores JOYP por Source y Selección

Se añadieron 6 contadores que distinguen entre reads desde programa vs cpu_poll, y entre buttons selected, dpad selected, o none selected.

Fase E: Intelligent Autopress

Se implementó autopress basado en eventos en src/viboy.py que:

  • Activa START cuando detecta write a JOYP con buttons selected (bit 5 = 0)
  • Libera START después de read con buttons selected o timeout de 60 frames

Tests y Verificación

Tests Clean-Room LDH

Comando: pytest tests/test_ldh_a8_ge_0x80_0486.py -v

Resultado:

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 ===============================

Validación: Los tests confirman que LDH calcula correctamente la dirección efectiva cuando a8 >= 0x80. No hay bug de direccionamiento en la implementación base.

Resultados

Mario (mario.gbc) - 300 frames

Evidencia de writes a FF92: ✅ Logs muestran [HRAM-WRITE] Write FF92=00 PC:1288

Tracking estructurado: ⚠️ HRAM_FF92_WriteCount=0 en snapshots (posible issue con gating o contador)

IE value: ⚠️ Permanece en 0x00 durante toda la ejecución

Conclusión: No hay evidencia de bug de direccionamiento en LDH. El problema parece estar en la cadena FF92→IE, donde IE no se está actualizando correctamente después de escribir en FF92.

Tetris DX (tetris_dx.gbc) - 300 frames

JOYP writes: ✅ ROM escribe 0x30 (buttons selected) frecuentemente (17811 writes en 240 frames)

JOYP reads: ⚠️ Reads muestran select_bits=0x03 (ninguna selección activa) y low_nibble=0x0F (todos los botones "soltados")

Conclusión: La ROM está escribiendo para seleccionar buttons, pero los reads ocurren cuando la selección ya se ha desactivado. Esto sugiere un problema de timing o de preservación del estado de selección entre write y read.

Archivos Afectados

  • src/core/cpp/CPU.hpp - Añadido LDHAddressWatch struct y getters
  • src/core/cpp/CPU.cpp - Tracking LDH en opcodes 0xE0 y 0xF0
  • src/core/cpp/MMU.hpp - Añadidos HRAMFF92Watch, FF92ToIETrace, actualizado JOYPTraceEvent
  • src/core/cpp/MMU.cpp - Implementación de tracking FF92, IE, y JOYP
  • src/core/cython/cpu.pyx, cpu.pxd - Wrappers Cython para getters LDH
  • src/core/cython/mmu.pyx, mmu.pxd - Wrappers Cython para getters FF92, IE, JOYP
  • tests/test_ldh_a8_ge_0x80_0486.py - Tests clean-room para LDH
  • tools/rom_smoke_0442.py - Añadidos campos IE/IME/IF a snapshots
  • src/viboy.py - Implementado intelligent autopress

Próximos Pasos

  1. Mario: Investigar por qué HRAM_FF92_WriteCount permanece en 0 a pesar de los logs. Verificar gating VIBOY_DEBUG_MARIO_FF92=1.
  2. Mario: Investigar por qué IE no se actualiza después de escribir en FF92. Verificar secuencia FF92→IE usando get_ff92_to_ie_trace().
  3. Tetris DX: Ejecutar con intelligent autopress activo para recopilar evidencia de reads con START bit = 0.
  4. Tetris DX: Investigar timing entre writes y reads de JOYP. Verificar si hay un delay entre write y read que cause que la selección se desactive.