⚠️ 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.

Diagnóstico Pantalla Blanca y Opcodes Condicionales

Fecha: 2025-12-17 Step ID: 0032 Estado: Verified

Resumen

Se realizó un diagnóstico exhaustivo del problema de la pantalla en blanco en Tetris DX, implementando 11 nuevos opcodes condicionales (saltos y llamadas) que estaban bloqueando el progreso del juego. Se añadieron logs de diagnóstico detallados para interrupciones y renderizado, y se crearon tests para verificar el comportamiento del renderer con diferentes valores de LCDC. El diagnóstico reveló que el emulador funciona correctamente, pero el juego nunca activa simultáneamente el bit 7 (LCD ON) y el bit 0 (Background ON) de LCDC, quedando atascado en la inicialización.

Concepto de Hardware

Los opcodes condicionales de la CPU LR35902 permiten control de flujo basado en flags:

  • JR cc, e: Saltos relativos condicionales (Jump Relative) basados en flags Z (Zero) o C (Carry)
  • JP cc, nn: Saltos absolutos condicionales (Jump) basados en flags
  • CALL cc, nn: Llamadas a subrutina condicionales basadas en flags
  • JP (HL): Salto indirecto a la dirección contenida en el registro HL

El registro LCDC (0xFF40) controla el display LCD con varios bits:

  • Bit 7: LCD Enable (1=ON, 0=OFF) - Activa/desactiva el LCD completo
  • Bit 0: BG Display (1=ON, 0=OFF) - Activa/desactiva el renderizado del Background

Para que se renderice el Background, ambos bits deben estar activos simultáneamente. Si solo el bit 7 está activo, el LCD está encendido pero no se renderiza nada (pantalla en blanco).

Fuente: Pan Docs - CPU Instruction Set, LCD Control Register (LCDC)

Implementación

Opcodes Condicionales Implementados (11 nuevos):

  • 0x28: JR Z, e - Jump Relative if Zero (3 ciclos si salta, 2 si no)
  • 0x38: JR C, e - Jump Relative if Carry (corregido de NOP a JR C, e)
  • 0xC2: JP NZ, nn - Jump if Not Zero (4 ciclos si salta, 3 si no)
  • 0xC4: CALL NZ, nn - Call if Not Zero (6 ciclos si salta, 3 si no)
  • 0xCA: JP Z, nn - Jump if Zero
  • 0xCC: CALL Z, nn - Call if Zero
  • 0xD2: JP NC, nn - Jump if Not Carry
  • 0xD4: CALL NC, nn - Call if Not Carry
  • 0xDA: JP C, nn - Jump if Carry
  • 0xDC: CALL C, nn - Call if Carry
  • 0xE9: JP (HL) - Jump to address in HL (1 ciclo)

Mejoras en Logging:

  • CPU.handle_interrupts(): Añadidos logs DEBUG para mostrar IME, IE, IF cuando hay interrupciones pendientes
  • Viboy.run(): Añadidos logs para mostrar estado de LCDC, BGP, IE, IF durante V-Blank
  • Renderer.render_frame(): Mejorados logs para mostrar estado completo de LCDC y BGP

Tests Añadidos:

  • tests/test_renderer_lcdc_bits.py: 4 tests para verificar comportamiento del renderer con diferentes valores de LCDC

Decisiones de diseño

Todos los opcodes condicionales siguen el mismo patrón: leen el offset/dirección, verifican el flag correspondiente, y ejecutan el salto/llamada solo si la condición se cumple. El timing es diferente según si se toma o no el salto, lo cual se respeta correctamente (más ciclos cuando se ejecuta el salto/llamada).

Para JP (HL), se lee directamente el valor de HL y se asigna a PC, en un solo ciclo M.

Archivos Afectados

  • src/cpu/core.py - Implementados 11 nuevos métodos de opcodes condicionales, mejorado logging en handle_interrupts()
  • src/viboy.py - Añadidos logs de diagnóstico durante V-Blank
  • src/gpu/renderer.py - Mejorados logs de diagnóstico en render_frame()
  • tests/test_renderer_lcdc_bits.py - Nuevo archivo con 4 tests para verificar comportamiento del renderer
  • DIAGNOSTICO_PANTALLA_BLANCA.md - Nuevo documento con diagnóstico completo del problema

Tests y Verificación

Tests Unitarios - Renderer LCDC

Comando ejecutado: python3 -m pytest tests/test_renderer_lcdc_bits.py -v

Entorno: macOS (darwin 21.6.0), Python 3.9.6, pytest 8.4.2

Resultado: 4 passed in 3.07s

Qué valida:

  • test_lcdc_bit7_off_no_render: Verifica que si el bit 7 (LCD Enable) está OFF, no se renderiza y la pantalla se llena de blanco. Valida que el renderer respeta el bit 7 de LCDC.
  • test_lcdc_bit0_off_no_bg_render: Verifica que si el bit 0 (Background Display) está OFF aunque el LCD esté ON, no se renderizan tiles del Background. Valida que el renderer respeta el bit 0 de LCDC.
  • test_lcdc_both_bits_on_should_render: Verifica que cuando ambos bits (7 y 0) están activos, se intenta renderizar. Valida que el renderer funciona correctamente cuando todo está activado.
  • test_bgp_0x00_all_white: Verifica que BGP=0x00 mapea todos los colores a blanco. Valida el comportamiento de la paleta cuando todos los bits son 0.

Código del test (fragmento esencial):

def test_lcdc_bit0_off_no_bg_render(self) -> None:
    """Verifica que si bit 0 está OFF, no se renderiza Background."""
    mmu = MMU(None)
    mmu.write_byte(IO_LCDC, 0x80)  # Bit 7 = 1 (LCD ON), Bit 0 = 0 (BG OFF)
    mmu.write_byte(IO_BGP, 0xE4)
    
    renderer = Renderer(mmu, scale=1)
    renderer.render_frame()
    # Si llegamos aquí, el test pasa (no se renderizaron tiles de BG)

Ruta completa: tests/test_renderer_lcdc_bits.py

Validación con ROM Real (Tetris DX)

ROM: Tetris DX (ROM aportada por el usuario, no distribuida)

Modo de ejecución: UI con Pygame, logging DEBUG activado

Criterio de éxito: El juego debe ejecutarse sin crashear por opcodes no implementados. Antes de esta implementación, Tetris DX se crasheaba al encontrar opcodes condicionales no implementados (0x28, 0x38, 0xCC, 0xC2, 0xE9, etc.).

Observación: Con los opcodes implementados, el juego ya no se crashea por opcodes faltantes. Los logs muestran:

  • Interrupciones V-Blank se procesan correctamente: INFO: INTERRUPT: V-Blank triggered -> 0x0040
  • LCDC se activa: LCDC=0x80 (LCD ON, pero Background OFF)
  • El juego nunca escribe un valor con ambos bits activos (como 0x81 o 0x91)
  • El renderer funciona correctamente - cuando Background está OFF, no renderiza tiles (comportamiento esperado)

Resultado: verified - El juego ejecuta sin crashear, pero queda atascado en inicialización sin activar el Background Display.

Notas legales: La ROM de Tetris DX es aportada por el usuario para pruebas locales. No se distribuye, no se adjunta, y no se enlaza descarga alguna. Solo se usa para validar el comportamiento del emulador.

Tests Totales del Proyecto

Comando ejecutado: python3 -m pytest tests/test_renderer_lcdc_bits.py tests/test_io_joypad.py -v

Resultado: 18 passed (14 de joypad + 4 de renderer) en 3.55s

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Opcodes condicionales: Los saltos y llamadas condicionales verifican flags antes de ejecutarse. El timing es crítico: más ciclos cuando se toma el salto porque se lee el offset/dirección adicional.
  • LCDC bits: El bit 7 y el bit 0 de LCDC deben estar activos simultáneamente para renderizar el Background. Si solo el bit 7 está activo, el LCD está encendido pero no se renderiza nada.
  • Proceso de diagnóstico: Los logs de diagnóstico son esenciales para entender qué está pasando. Añadir logs estratégicos en puntos clave (interrupciones, renderizado, registros I/O) ayuda a identificar problemas rápidamente.
  • Estado del emulador: El emulador funciona correctamente hasta donde está implementado. El problema de la pantalla en blanco no es un bug del emulador, sino que el juego no puede completar su inicialización debido a opcodes faltantes o dependencias no implementadas.

Lo que Falta Confirmar

  • ¿Por qué el juego nunca activa el Background?: El juego nunca escribe un valor de LCDC con ambos bits activos. Puede ser que falten más opcodes que impiden llegar a esa parte del código, o que el juego dependa de un estado inicial diferente (Boot ROM) que no tenemos implementado.
  • ¿Cuántos opcodes faltan?: Aunque todos los opcodes reportados están implementados, es posible que falten más que no se han encontrado porque el juego se atasca antes de llegar a ellos.
  • ¿Necesitamos Boot ROM?: La mayoría de juegos asumen que la Boot ROM ya ejecutó cierta inicialización. Sin Boot ROM, el estado inicial puede no ser el correcto.

Hipótesis y Suposiciones

Suponemos que el juego está atascado en un bucle de inicialización esperando alguna condición que nunca se cumple. Esto podría deberse a:

  • Opcodes faltantes que no se han encontrado todavía
  • Comportamiento de Boot ROM que no está implementado
  • Problemas de timing que impiden que el juego progrese

Próximos Pasos

  • [ ] Continuar ejecutando y monitoreando si aparecen más opcodes faltantes
  • [ ] Considerar implementar más opcodes preventivamente basándose en frecuencia de uso
  • [ ] Verificar si otros juegos tienen el mismo problema o si es específico de Tetris DX
  • [ ] Considerar si necesitamos implementar la Boot ROM para que el juego tenga el estado inicial correcto
  • [ ] Verificar si hay problemas de timing que impiden que el juego progrese