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.
- 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_tiletest_signed_addressing_fixtest_sprite_rendering_simpletest_sprite_transparencytest_sprite_x_fliptest_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_countertest_stack_pointertest_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:
- Foundation first: Resolver la discrepancia de inicialización de registros antes que PPU
- Menor superficie de cambio: Solo tocar tests, no core (3 tests en 1 archivo)
- Decisión de diseño clara: Documentar la política Post-Boot vs Zero-Init
- 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()aPyRegisterspara 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:
- Step 0427: Cluster B (Registers) - Foundation
- Step 0428: Cluster C (CPU Control) - Low-hanging fruit
- 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