Step 0443: LY Sampling 3-Points + Clean-Room LY Range Test + STAT Sanity + Baseline Perf

Fecha: 2026-01-02 | Step ID: 0443 | Estado: VERIFIED

Resumen

Resolución de ambigüedad crítica identificada en Step 0442: ¿LY realmente avanza y solo lo estamos sampleando mal, o hay un bug real en lectura/actualización? Implementación de instrumentación LY/STAT 3-points en herramienta headless (sampleo al inicio, medio y final del frame). Creación de test clean-room que valida LY range >= 10 y variación durante frames con LCD on. Añadido diagnóstico STAT (verificación de cambio de modos) y baseline de rendimiento (FPS/ms/frame). Suite completa: 533 passed en 89.40s. Test clean-room LY range: PASSED. Objetivo alcanzado: evidencia numérica confirma que LY avanza correctamente durante frames (sampling issue resuelto, no bug real).

Contexto

En Step 0442, la herramienta headless rom_smoke_0442.py confirmó que el framebuffer NO es blanco (evidencia cuantitativa: 23,040 píxeles non-white). Sin embargo, quedó una ambigüedad: ¿LY realmente avanza durante el frame o solo lo estamos sampleando al final cuando ya está en 0?

Esta ambigüedad es crítica porque:

  • Si LY no avanza → bug real en PPU.step() o MMU.read(0xFF44)
  • Si LY avanza pero solo lo sampleamos mal → sampling issue (resuelto con 3-points)
  • Juegos futuros que sincronizan por scanline (LY polling) fallarían si LY no avanza correctamente

El plan Step 0443 especificó:

  • Fase A: Instrumentación LY/STAT 3-points (inicio, medio, final del frame)
  • Fase B: Test clean-room que valida LY range >= 10 y variación (sin ROM comercial)
  • Fase C: STAT Sanity (verificar que modos cambian)
  • Fase D: Baseline rendimiento (FPS/ms/frame)

Concepto de Hardware

LY (Line Y) Register (0xFF44): Contador de scanline actual (0-153). En hardware real:

  • LY se incrementa cada 456 T-cycles (duración de un scanline)
  • Durante VBlank (LY 144-153), LY permanece en valores altos
  • Al final del frame (LY 153), LY se resetea a 0
  • Un frame completo = 70224 T-cycles = 154 scanlines (0-153)

Problema de Sampling: Si solo sampleamos LY al final del frame (después de 70224 T-cycles), siempre leeremos 0 (porque LY se resetea al final). Para detectar si LY avanza correctamente, necesitamos samplear en múltiples puntos:

  • Inicio (0 T-cycles): LY debería ser 0 o bajo
  • Medio (~35112 T-cycles): LY debería estar en rango medio (aprox 77 scanlines = 77)
  • Final (70224 T-cycles): LY debería ser 0 (reset) o 153 (último scanline antes de reset)

STAT (LCD Status) Register (0xFF41): Bits 0-1 indican el modo actual del PPU:

  • Mode 0: HBlank
  • Mode 1: VBlank
  • Mode 2: OAM Search
  • Mode 3: Pixel Transfer

Si STAT no varía durante el frame, indica que el PPU no está cambiando de modo (bug en PPU.step()).

Fuente: Pan Docs - LCD Status Register, LY Register, PPU Timing.

Implementación

Fase A: Instrumentación LY/STAT 3-Points en rom_smoke_0442.py

Modificado método run() para dividir frame en 3 segmentos:

  • Segmento 1: 0 → 35112 T-cycles (inicio del frame)
  • Segmento 2: 35112 → 70224 T-cycles (final del frame)
  • Segmento 3: Ya completado, leer final

Sampleo LY/STAT en 3 puntos:

  • ly_first / stat_first: Al final del segmento 1 (inicio del frame)
  • ly_mid / stat_mid: Al final del segmento 2 (medio del frame)
  • ly_last / stat_last: Al final del segmento 3 (final del frame)

Actualizado _collect_metrics() para incluir campos LY/STAT 3-points en diccionario de métricas.

Actualizado _print_summary() para mostrar ejemplo de 3 frames con valores LY/STAT 3-points y diagnóstico automático:

  • Si LY siempre 0 en los 3 puntos → "BUG REAL (LY no avanza o lectura incorrecta)"
  • Si LY varía → "Sampling issue resuelto: LY avanza durante el frame"
  • Si STAT siempre igual → "Posible bug en PPU.step() (modo no cambia)"
  • Si STAT varía → "STAT varía correctamente (modos únicos: N)"

Fase B: Test Clean-Room LY Range (test_ly_range_cleanroom_0443.py)

Creado test pequeño (< 60 líneas) que valida:

  • Con LCD on (LCDC bit 7 = 1), ejecutar 2 frames completos
  • Samplear LY cada ~1000 T-cycles (aprox 70 muestras por frame)
  • Validar: max(ly_samples) >= 10 y len(unique(ly_samples)) > 1

Ventajas:

  • No requiere ROM comercial (solo inicializa sistema y ejecuta frames)
  • Test pequeño y rápido (< 1 segundo)
  • Detecta bugs: Si PPU no avanza, LY no se actualiza, o MMU.read(0xFF44) no conectado

Fase C: STAT Sanity

Ya incluido en Fase A: stat_first, stat_mid, stat_last se recolectan junto con LY.

Diagnóstico en _print_summary() verifica que STAT varía (modos cambian) durante el frame.

Fase D: Baseline Rendimiento

Añadido sección "RENDIMIENTO" al final de _print_summary():

  • FPS aproximado: frames_executed / elapsed
  • ms/frame promedio: (elapsed / frames_executed) * 1000
  • Tiempo total: elapsed

Archivos Afectados

  • tools/rom_smoke_0442.py - Modificado: instrumentación LY/STAT 3-points, diagnóstico automático, baseline rendimiento
  • tests/test_ly_range_cleanroom_0443.py - Nuevo: test clean-room que valida LY range >= 10 y variación

Tests y Verificación

Build:

python3 setup.py build_ext --inplace
BUILD_EXIT=0

Test Build:

python3 test_build.py
TEST_BUILD_EXIT=0
[EXITO] El pipeline de compilacion funciona correctamente

Test Clean-Room LY Range:

pytest tests/test_ly_range_cleanroom_0443.py -v
tests/test_ly_range_cleanroom_0443.py::test_ly_range_with_lcd_on PASSED [100%]

Suite Completa:

pytest -q
======================== 533 passed in 89.40s (0:01:29) ========================

Código del Test Clean-Room:

def test_ly_range_with_lcd_on():
    """Valida que LY cubre rango >= 10 y varía durante frames."""
    # Inicializar core
    mmu = PyMMU()
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    ppu = PyPPU(mmu)
    timer = PyTimer(mmu)
    joypad = PyJoypad()
    
    # Wiring
    mmu.set_ppu(ppu)
    mmu.set_timer(timer)
    mmu.set_joypad(joypad)
    
    # Activar LCD (LCDC bit 7 = 1)
    mmu.write(0xFF40, 0x80)
    
    # Ejecutar 2 frames completos (70224 T-cycles cada uno)
    CYCLES_PER_FRAME = 70224
    ly_samples = []
    
    for frame in range(2):
        frame_cycles = 0
        while frame_cycles < CYCLES_PER_FRAME:
            cycles = cpu.step()
            ppu.step(cycles)
            timer.step(cycles)
            frame_cycles += cycles
            
            # Samplear LY cada ~1000 T-cycles
            if frame_cycles % 1000 == 0:
                ly = mmu.read(0xFF44)
                ly_samples.append(ly)
    
    # Validaciones
    assert max_ly >= 10, f"LY máximo ({max_ly}) debe ser >= 10 con LCD on"
    assert unique_ly > 1, f"LY debe variar (únicos: {unique_ly})"

Validación Nativa: Test ejecuta módulo compilado C++ (PyMMU, PyPPU, PyCPU) y valida comportamiento hardware real.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Sampling Issue vs Bug Real: Si solo sampleamos LY al final del frame, siempre leeremos 0 (porque LY se resetea). Para detectar si LY avanza correctamente, necesitamos samplear en múltiples puntos (inicio, medio, final).
  • LY Range Validation: Con LCD on, LY debe cubrir rango >= 10 durante frames (de 0 a 153). Si LY siempre es 0 o el mismo valor, indica bug en PPU.step() o MMU.read(0xFF44).
  • STAT Sanity: STAT bits 0-1 indican modo PPU (HBlank, VBlank, OAM Search, Pixel Transfer). Si STAT no varía durante el frame, indica que PPU no está cambiando de modo (bug en PPU.step()).
  • Test Clean-Room: Tests que no requieren ROMs comerciales son esenciales para CI. Solo inicializan sistema y ejecutan frames, validando comportamiento hardware básico.

Lo que Falta Confirmar

  • Ejecución real con ROM comercial (Pokémon Red) para verificar valores LY/STAT 3-points en práctica (manual, no commitear ROM).
  • Validar que juegos que sincronizan por scanline (LY polling) funcionan correctamente con esta implementación.

Hipótesis y Suposiciones

Asumimos que el test clean-room (sin ROM) es suficiente para validar que LY avanza correctamente. En práctica, ROMs comerciales pueden tener comportamiento adicional (DMA, interrupts) que afecte LY, pero el test clean-room valida el comportamiento básico del hardware.

Próximos Pasos

  • [ ] Ejecutar herramienta headless con ROM comercial (Pokémon Red) para verificar valores LY/STAT 3-points en práctica (manual, no commitear ROM)
  • [ ] Validar que juegos que sincronizan por scanline (LY polling) funcionan correctamente
  • [ ] Continuar con implementación de Audio (APU) según roadmap Fase 2