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

Timer (DIV) y Limpieza de Logs

Fecha: 2025-12-18 Step ID: 0037 Estado: Verified

Resumen

El emulador sufría de rendimiento inaceptable debido a logs excesivos que bloqueaban el hilo principal (especialmente en MacBook Air 2015). Se silenciaron los logs de nivel INFO dentro del bucle crítico (MMU y renderer), cambiándolos a DEBUG. Además, se implementó el subsistema Timer con el registro DIV (0xFF04), que es crítico para juegos como Tetris DX que lo usan para generación de números aleatorios (RNG). El Timer incrementa continuamente a 16384 Hz (cada 256 T-Cycles) y se resetea cuando se escribe en 0xFF04.

Concepto de Hardware

El Timer de la Game Boy es un sistema de temporización que incluye varios registros:

  • DIV (0xFF04): Divider Register - Contador que incrementa continuamente a velocidad fija (16384 Hz).
  • TIMA (0xFF05): Timer Counter - Contador configurable que puede generar interrupciones.
  • TMA (0xFF06): Timer Modulo - Valor de recarga cuando TIMA desborda.
  • TAC (0xFF07): Timer Control - Controla si TIMA está activo y su frecuencia.

En este paso, implementamos solo DIV, que es el más crítico para muchos juegos.

Registro DIV (0xFF04)

DIV es un contador interno de 16 bits que incrementa a velocidad fija: 16384 Hz. El registro DIV expone solo los 8 bits altos del contador interno (bits 8-15).

Características clave:

  • Incrementa cada 256 T-Cycles (4.194304 MHz / 16384 Hz = 256).
  • Cualquier escritura en DIV resetea el contador interno a 0, independientemente del valor escrito.
  • Muchos juegos usan DIV para generar números aleatorios (RNG) leyendo su valor en momentos impredecibles.
  • El contador interno hace wrap-around automáticamente (0xFFFF → 0x0000).

Impacto del Logging: Imprimir en stdout es una operación lentísima que bloquea el hilo principal. En un MacBook Air 2015, escribir miles de mensajes por segundo en la consola puede reducir el rendimiento de 60 FPS a menos de 1 FPS. Por eso, los logs de nivel INFO dentro del bucle crítico deben cambiarse a DEBUG para que solo aparezcan cuando se active explícitamente el modo debug.

Fuente: Pan Docs - Timer and Divider Registers

Implementación

1. Limpieza de Logs

Se cambiaron los siguientes logs de INFO a DEBUG:

  • MMU (src/memory/mmu.py): Log "IO WRITE" que se ejecutaba cada vez que se escribía en un registro I/O (0xFF00-0xFF7F).
  • Renderer (src/gpu/renderer.py): Log "LCDC: LCD desactivado" que se ejecutaba cada frame cuando el LCD estaba apagado.
  • Viboy (src/viboy.py): Log "V-Blank" que se ejecutaba cada frame durante V-Blank.

El heartbeat (cada 60 frames ≈ 1 segundo) se mantiene en INFO para confirmar que el emulador está vivo.

2. Implementación del Timer

Se creó la clase Timer en src/io/timer.py con los siguientes métodos:

  • tick(t_cycles): Avanza el Timer según los T-Cycles transcurridos. Acumula ciclos en el contador interno.
  • read_div(): Lee el registro DIV (8 bits altos del contador interno).
  • write_div(value): Resetea el contador interno a 0. El valor escrito se ignora.
  • get_div_counter(): Obtiene el valor completo del contador interno (16 bits) para tests.

Implementación del contador: El contador interno es de 16 bits y se incrementa acumulando T-Cycles. DIV expone solo los 8 bits altos: DIV = (div_counter >> 8) & 0xFF.

3. Integración en MMU

Se añadió soporte para leer/escribir DIV (0xFF04) en la MMU:

  • En read_byte(0xFF04): Se intercepta la lectura y se delega al Timer.
  • En write_byte(0xFF04): Se intercepta la escritura y se delega al Timer (resetea el contador).
  • Se añadió el método set_timer() para conectar el Timer a la MMU (evitar dependencia circular).

4. Integración en Viboy

Se integró el Timer en el sistema principal:

  • Se crea una instancia de Timer en __init__() y load_cartridge().
  • Se conecta el Timer a la MMU mediante mmu.set_timer().
  • En el método tick(), se llama a timer.tick(t_cycles) después de ejecutar la instrucción de la CPU.

Nota: Por ahora, solo se implementa DIV. TIMA/TMA/TAC se añadirán más adelante cuando sean necesarios.

Archivos Afectados

  • src/io/timer.py - Nueva clase Timer con registro DIV
  • src/io/__init__.py - Exporta Timer
  • src/memory/mmu.py - Integración de Timer (lectura/escritura de 0xFF04), silenciado de logs IO WRITE
  • src/gpu/renderer.py - Silenciado de logs "LCDC desactivado"
  • src/viboy.py - Integración de Timer en sistema principal, silenciado de logs V-Blank
  • tests/test_io_timer.py - Suite de tests para Timer (10 tests)

Tests y Verificación

Se creó una suite completa de tests para el Timer con 10 tests que validan:

  • Inicialización correcta (DIV = 0)
  • Incremento de DIV cada 256 T-Cycles
  • Incremento múltiple y wrap-around
  • Reset de DIV al escribir en 0xFF04
  • Integración con MMU (lectura/escritura)

Ejecución de Tests

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

Entorno: macOS (darwin 21.6.0), Python 3.9.6

Resultado:10 tests PASSED en 0.46s

Qué valida:

  • El Timer incrementa correctamente a 16384 Hz (cada 256 T-Cycles).
  • DIV expone solo los 8 bits altos del contador interno de 16 bits.
  • Cualquier escritura en DIV resetea el contador interno, independientemente del valor escrito.
  • La integración con MMU funciona correctamente (lectura/escritura de 0xFF04).

Código de Tests

Fragmento clave del test que valida el incremento de DIV:

def test_div_increment(self) -> None:
    """Test: Verificar que DIV incrementa cada 256 T-Cycles"""
    timer = Timer()
    
    # DIV debe empezar en 0
    assert timer.read_div() == 0
    
    # Avanzar 255 T-Cycles (aún no debe incrementar)
    timer.tick(255)
    assert timer.read_div() == 0
    
    # Avanzar 1 T-Cycle más (total 256) -> DIV debe incrementar
    timer.tick(1)
    assert timer.read_div() == 1
    
    # Avanzar otros 256 T-Cycles -> DIV debe ser 2
    timer.tick(256)
    assert timer.read_div() == 2

Por qué este test demuestra el comportamiento del hardware: El test verifica que DIV incrementa exactamente cada 256 T-Cycles, que es la frecuencia correcta (16384 Hz) según la especificación del hardware. Además, valida que el contador interno se acumula correctamente y que DIV expone solo los 8 bits altos.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • DIV es crítico para RNG: Muchos juegos usan DIV para generar números aleatorios leyendo su valor en momentos impredecibles. Si DIV siempre devuelve 0, los juegos pueden quedarse esperando o generar secuencias predecibles.
  • El logging mata el rendimiento: Escribir en stdout es extremadamente lento y bloquea el hilo principal. En hardware antiguo (MacBook Air 2015), esto puede reducir el rendimiento de 60 FPS a menos de 1 FPS.
  • DIV expone solo 8 bits altos: El contador interno es de 16 bits, pero DIV solo expone los bits 8-15. Esto significa que DIV incrementa cada 256 T-Cycles (cuando el bit 8 del contador cambia).
  • Reset de DIV: Cualquier escritura en DIV resetea el contador interno a 0, independientemente del valor escrito. Este es un comportamiento del hardware real, no un bug.

Lo que Falta Confirmar

  • TIMA/TMA/TAC: Por ahora solo implementamos DIV. TIMA (Timer Counter) y TAC (Timer Control) se implementarán más adelante cuando sean necesarios para juegos específicos. TIMA puede generar interrupciones cuando desborda, lo cual es más complejo.
  • Valor inicial de DIV: En una Game Boy real, el contador interno puede tener un valor aleatorio al encender. Por ahora, lo inicializamos a 0 para reproducibilidad en tests. Esto podría afectar a juegos que dependen del valor inicial para RNG.
  • Impacto en rendimiento real: Aunque silenciamos los logs, aún no hemos probado el emulador con una ROM real (como Tetris DX) para confirmar que el rendimiento mejora significativamente. Esto se validará en el siguiente paso.

Hipótesis y Suposiciones

Suposición sobre frecuencia de DIV: Asumimos que DIV incrementa cada 256 T-Cycles basándonos en la documentación (16384 Hz = 4194304 Hz / 256). Esta suposición está respaldada por Pan Docs, pero no la hemos verificado con hardware real o test ROMs específicas.

Suposición sobre reset de DIV: Asumimos que cualquier escritura en DIV resetea el contador interno, independientemente del valor escrito. Esta suposición está respaldada por Pan Docs y es un comportamiento común en hardware de la época.

Próximos Pasos

  • [ ] Probar el emulador con Tetris DX para confirmar que el rendimiento mejora significativamente
  • [ ] Verificar que DIV se lee correctamente durante la ejecución del juego
  • [ ] Confirmar que el LCD se enciende después de la carga inicial (ahora que el rendimiento debería ser normal)
  • [ ] Implementar TIMA/TMA/TAC si son necesarios para juegos específicos
  • [ ] Ajustar el valor inicial de DIV si es necesario para compatibilidad con juegos que dependen de RNG