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

Bitácora del Proyecto Viboy Color

← Volver al índice

Step 0426: Triage 10 Fallos + Clustering

📋 Resumen Ejecutivo

Triage completo y sistemático de los 10 tests fallidos restantes tras Step 0425. Captura exacta de fallos, análisis de causa raíz por cluster y selección de estrategia de fix atómico. Decisión crítica: NO tocar código en este Step, solo diagnóstico riguroso.

Resultado del Clustering:
  • Cluster A: 6 fallos PPU (framebuffer swap bug) - 🔴 ALTA prioridad
  • Cluster B: 3 fallos Registers (Post-Boot vs Zero-Init) - 🟡 MEDIA prioridad
  • Cluster C: 1 fallo CPU Control (EI delay test mal) - 🟢 BAJA prioridad

Cluster seleccionado para Step 0427: Cluster B (Foundation first, menor superficie)

🔧 Concepto de Hardware

1. Post-Boot State (Pan Docs - Power Up Sequence)

Cuando la Game Boy se enciende, la Boot ROM ejecuta una secuencia de inicialización y deja los registros en un estado específico antes de saltar al código del cartucho (0x0100):

  • DMG: A=0x01, BC=0x0013, DE=0x00D8, HL=0x014D, SP=0xFFFE, PC=0x0100, F=0xB0
  • CGB: A=0x11, BC=0x0000, DE=0xFF56, HL=0x000D, SP=0xFFFE, PC=0x0100, F=0x80

El core de Viboy Color implementa Post-Boot State por defecto (skip-boot), lo que significa que PyRegisters() inicia con PC=0x0100, no PC=0x0000.

2. EI Delay (Pan Docs - CPU Instruction Set)

La instrucción EI (Enable Interrupts) tiene un comportamiento crítico: delay de 1 instrucción. Esto significa que IME no se activa inmediatamente, sino después de ejecutar la siguiente instrucción.

; Ejemplo hardware-accurate:
EI          ; IME sigue False aquí
NOP         ; Esta instrucción se ejecuta con IME=False
; Aquí IME se activa automáticamente
; Interrupciones pendientes se procesan

Esto es crítico para patrones como EI + RETI usados en handlers de interrupción.

3. PPU Framebuffer Double-Buffering

El emulador usa double-buffering para evitar tearing:

  • Back buffer: Donde la PPU escribe píxeles durante el rendering (línea por línea)
  • Front buffer: Buffer expuesto a Python/SDL para display
  • Swap: Al final del frame (LY=144), se debe copiar back → front

El bug detectado: el back buffer tiene píxeles correctos, pero el front buffer queda en blanco (swap no funciona).

⚙️ Implementación Técnica

Tarea T1: Captura Exacta de Fallos

Comandos ejecutados:

pytest -q > /tmp/viboy_0426_pytest.log 2>&1
tail -n 140 /tmp/viboy_0426_pytest.log
grep -n "^FAILED " /tmp/viboy_0426_pytest.log

# Fallos individuales por cluster:
pytest -vv tests/test_core_ppu_rendering.py::TestCorePPURendering::test_bg_rendering_simple_tile --maxfail=1 -x > /tmp/viboy_0426_ppu_rendering_first.log 2>&1
pytest -vv tests/test_core_registers.py::TestPyRegistersPCSP::test_program_counter --maxfail=1 -x > /tmp/viboy_0426_registers_first.log 2>&1
pytest -vv tests/test_cpu_control.py::TestCPUControl::test_di_ei_sequence --maxfail=1 -x > /tmp/viboy_0426_cpu_control_first.log 2>&1

Análisis de Causa Raíz

Cluster A: PPU Framebuffer Swap (6 fallos)

Tests afectados:

  • test_bg_rendering_simple_tile
  • test_signed_addressing_fix
  • test_sprite_rendering_simple
  • test_sprite_transparency
  • test_sprite_x_flip
  • test_sprite_palette_selection

Assertion típica:

AssertionError: Primer píxel debe ser negro (0xFF000000), es 0xFFFFFFFF (índice=0)
assert 4294967295 == 4278190080

Evidencia del log:

[PPU-RENDER-WRITE] First 20 pixels: 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
[PPU-FRAMEBUFFER-LINE] Pixel (0, 0): index=0  # ❌ Debería ser 3

Diagnóstico: ✅ Bug de core - El renderer escribe correctamente en el back buffer, pero el buffer final expuesto a Python queda en blanco. Problema en renderer.py (swap/copy de buffers).

Cluster B: Registers Post-Boot vs Zero-Init (3 fallos)

Tests afectados:

  • test_program_counter
  • test_stack_pointer
  • test_inicializacion_por_defecto

Assertion típica:

def test_program_counter(self):
    reg = PyRegisters()
    assert reg.pc == 0  # ❌ Falla: pc=0x0100 (256)

Código del core (Registers.cpp:33):

CoreRegisters::CoreRegisters() :
    a(0x01), b(0x00), c(0x13), d(0x00), e(0xD8),
    h(0x01), l(0x4D), f(0xB0),
    pc(0x0100),  // Post-Boot State (Pan Docs)
    sp(0xFFFE)

Diagnóstico: ❌ Tests mal diseñados - El core implementa correctamente el Post-Boot State (PC=0x0100 según Pan Docs). Los tests asumen inicialización a cero, lo cual contradice el diseño del hardware real.

Cluster C: CPU Control EI Delay (1 fallo)

Test afectado:

  • test_di_ei_sequence

Código del test:

cpu._op_ei()
assert cpu.ime is True  # ❌ Falla: ime=False

Código del core (core.py:2405):

def _op_ei(self) -> int:
    """EI tiene un retraso de 1 instrucción (Pan Docs)"""
    self.ime_scheduled = True  # No activar inmediatamente
    return 1

Diagnóstico: ❌ Test mal diseñado - El test llama a _op_ei() directamente y espera IME=True inmediato. El core implementa correctamente el delay de 1 instrucción (hardware-accurate según Pan Docs).

🎯 Decisión Estratégica

Cluster Seleccionado para Step 0427: Cluster B (Registers Post-Boot)

Justificación:

  1. Foundation first: Resolver la discrepancia de inicialización de registros antes que PPU
  2. Menor superficie de cambio: Solo tocar tests, no core (3 tests en 1 archivo)
  3. Decisión de diseño clara: Documentar la política Post-Boot vs Zero-Init
  4. No tocar PPU todavía: El Cluster A (PPU framebuffer) es más complejo y debe hacerse después de limpiar foundation

Estrategia propuesta:

  • Añadir método reset_to_zero() a PyRegisters para tests que necesiten estado crudo
  • Actualizar los 3 tests para usar reset_to_zero() o aceptar valores Post-Boot
  • Documentar la política en el test con comentarios Pan Docs
  • Verificación: build + test_build + pytest target + pytest global (215 → 218 passing)

Orden de resolución de clusters:

  1. Step 0427: Cluster B (Registers) - Foundation
  2. Step 0428: Cluster C (CPU Control) - Low-hanging fruit
  3. Step 0429+: Cluster A (PPU Framebuffer) - Requiere investigación profunda en renderer.py

✅ Tests y Verificación

Comando Ejecutado

pytest -q

Resultado

======================== 10 failed, 215 passed in 0.88s ========================

Validación

  • ✅ Captura exacta de 10 fallos (no se introdujeron nuevos fallos)
  • ✅ Clustering por causa raíz completado
  • ✅ Primer fallo por cluster analizado en detalle
  • ✅ Estrategia de fix atómico definida
  • ✅ NO se modificó código (triage puro)

Desglose de Fallos por Cluster

Cluster Fallos Causa Tipo Prioridad
A (PPU) 6 Framebuffer swap bug Bug de core 🔴 ALTA
B (Registers) 3 Post-Boot vs Zero-Init Tests mal 🟡 MEDIA
C (CPU Control) 1 EI delay test mal Test mal 🟢 BAJA

📁 Archivos Analizados

  • tests/test_core_ppu_rendering.py - 2 fallos (Cluster A)
  • tests/test_core_ppu_sprites.py - 4 fallos (Cluster A)
  • tests/test_core_registers.py - 3 fallos (Cluster B)
  • tests/test_cpu_control.py - 1 fallo (Cluster C)
  • src/core/cpp/Registers.cpp - Verificado (Post-Boot correcto)
  • src/cpu/core.py - Verificado (EI delay correcto)

📚 Lecciones Aprendidas

  • Triage disciplinado: NO modificar código en el Step de diagnóstico evita introducir nuevos fallos
  • Clustering por causa raíz: Agrupar fallos permite fixes atómicos y minimiza superficie de cambio
  • Foundation first: Resolver discrepancias de registros/CPU antes que PPU simplifica debugging posterior
  • Tests vs Spec: Cuando un test contradice Pan Docs, el test está mal (no el core)
  • Post-Boot State: El emulador debe decidir entre Zero-Init (para tests puros) o Post-Boot (para realismo)
  • Hardware-accurate delay: EI delay de 1 instrucción es crítico para juegos reales (no simplificar)

🔗 Referencias

  • Pan Docs - Power Up Sequence: Post-Boot State (DMG/CGB)
  • Pan Docs - CPU Instruction Set (EI): Delay de 1 instrucción
  • Pan Docs - Video Display Controller: Double-buffering y swap de framebuffer