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

Fix: Bug de Renderizado en Signed Addressing y Expansión de la ALU

Fecha: 2025-12-19 Step ID: 0138 Estado: Verified

Resumen

Se mejoró la validación de direcciones en el método render_scanline() de la PPU para prevenir Segmentation Faults cuando se calculan direcciones de tiles en modo signed addressing. La corrección asegura que tanto la dirección base del tile como la dirección de la línea del tile (incluyendo el byte siguiente) estén dentro de los límites de VRAM (0x8000-0x9FFF). Además, se verificó que el bloque completo de la ALU (0x80-0xBF) esté implementado correctamente, confirmando que todos los 64 opcodes de operaciones aritméticas y lógicas están disponibles para la ejecución de juegos.

Concepto de Hardware

La Game Boy tiene dos modos de direccionamiento para los tiles en VRAM:

  • Unsigned Addressing (bit 4 de LCDC = 1): Los tile IDs van de 0 a 255, y los tiles se almacenan desde la dirección 0x8000. Cada tile ID se multiplica por 16 (cada tile son 16 bytes) para obtener la dirección: dirección = 0x8000 + (tile_id * 16).
  • Signed Addressing (bit 4 de LCDC = 0): Los tile IDs se interpretan como valores con signo (-128 a 127), y el tile 0 está en la dirección 0x9000 (no en 0x8800). La fórmula es: dirección = 0x9000 + (signed_tile_id * 16).

El problema crítico es que cuando se usa signed addressing, un tile ID de 128 (0x80) se interpreta como -128, lo que resulta en una dirección de 0x9000 + (-128 * 16) = 0x9000 - 0x800 = 0x8800. Sin embargo, si el tile ID es muy negativo o muy positivo, el cálculo puede resultar en direcciones fuera de VRAM (menor que 0x8000 o mayor que 0x9FFF), causando Segmentation Faults cuando la PPU intenta leer esos datos.

Fuente: Pan Docs - Tile Data Addressing, LCD Control Register (LCDC)

Implementación

Se mejoró la validación de direcciones en PPU::render_scanline() para asegurar que:

  1. La dirección base del tile esté dentro de VRAM y tenga espacio suficiente para los 16 bytes del tile completo (verificando que tile_addr <= VRAM_END - 15).
  2. La dirección de la línea del tile (que se calcula como tile_addr + tile_y_offset * 2) y el byte siguiente (tile_line_addr + 1) estén ambos dentro de VRAM.

Esta validación doble previene accesos fuera de límites que causaban Segmentation Faults cuando la PPU intentaba renderizar tiles con IDs que resultaban en direcciones inválidas.

Componentes modificados

  • src/core/cpp/PPU.cpp: Mejora de la validación de direcciones en render_scanline() para cubrir todos los casos edge, incluyendo tiles que se extienden hasta el límite de VRAM.

Verificación del Bloque ALU

Se verificó que el bloque completo de la ALU (0x80-0xBF) esté implementado correctamente en CPU.cpp. Este bloque contiene 64 opcodes que implementan operaciones aritméticas y lógicas entre el registro A y otros registros o memoria:

  • 0x80-0x87: ADD A, r (Suma)
  • 0x88-0x8F: ADC A, r (Suma con Carry)
  • 0x90-0x97: SUB r (Resta)
  • 0x98-0x9F: SBC A, r (Resta con Carry)
  • 0xA0-0xA7: AND r (AND lógico)
  • 0xA8-0xAF: XOR r (XOR lógico)
  • 0xB0-0xB7: OR r (OR lógico)
  • 0xB8-0xBF: CP r (Comparar)

Todos los opcodes están implementados correctamente, incluyendo las variantes que acceden a memoria a través de (HL).

Decisiones de diseño

La validación mejorada usa comparaciones con límites ajustados (VRAM_END - 15 para tiles completos y VRAM_END - 1 para líneas de tiles) para asegurar que no solo la dirección base, sino también todos los bytes necesarios, estén dentro de VRAM. Esto es crítico porque un tile completo son 16 bytes, y una línea de tile son 2 bytes consecutivos.

Archivos Afectados

  • src/core/cpp/PPU.cpp - Mejora de la validación de direcciones en render_scanline() para prevenir Segmentation Faults en modo signed addressing

Tests y Verificación

La corrección se validó mediante:

  • Test existente: test_signed_addressing_fix en tests/test_core_ppu_rendering.py verifica que el cálculo de direcciones en modo signed es correcto y que no se producen Segmentation Faults.
  • Validación del bloque ALU: Se verificó mediante grep que todos los 64 opcodes del bloque 0x80-0xBF estén implementados en CPU.cpp.
  • Linter: No se encontraron errores de compilación o linter en el código modificado.

Comando de test

pytest tests/test_core_ppu_rendering.py::TestCorePPURendering::test_signed_addressing_fix -v

Código del test relevante

def test_signed_addressing_fix(self) -> None:
    """Verifica que el cálculo de dirección en modo signed es correcto."""
    mmu = PyMMU()
    ppu = PyPPU(mmu)
    
    # LCDC con bit 4=0 (signed addressing activo)
    mmu.write(0xFF40, 0x81)
    
    # Tile ID 128 (que se interpreta como -128 en modo signed)
    # Dirección esperada: 0x9000 + (-128 * 16) = 0x8800
    mmu.write(0x9800, 128)
    
    # Escribir tile en 0x8800
    for line in range(8):
        mmu.write(0x8800 + (line * 2), 0xFF)
        mmu.write(0x8800 + (line * 2) + 1, 0xFF)
    
    # Avanzar PPU hasta completar una línea
    ppu.step(456)
    
    # Verificar que no hay Segmentation Fault y que el renderizado es correcto
    framebuffer = ppu.get_framebuffer()
    assert framebuffer[0] == 3  # Color 3 (negro)

Validación de módulo compilado C++: El test valida que la PPU C++ puede renderizar tiles en modo signed addressing sin causar Segmentation Faults, confirmando que la validación de direcciones funciona correctamente.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Signed Addressing en PPU: El modo signed addressing usa 0x9000 como base (no 0x8800), y los tile IDs se interpretan como valores con signo (-128 a 127). Esto permite acceder a tiles tanto por encima como por debajo del tile 0, pero requiere validación cuidadosa para prevenir accesos fuera de VRAM.
  • Validación de Direcciones: Es crítico validar no solo la dirección base, sino también todos los bytes que se van a leer (en este caso, 2 bytes consecutivos para una línea de tile). Además, debemos considerar el tamaño completo del tile (16 bytes) al validar la dirección base.
  • Bloque ALU Completo: El bloque 0x80-0xBF contiene todas las operaciones aritméticas y lógicas fundamentales de la CPU. Cada operación tiene 8 variantes (una por cada registro B, C, D, E, H, L, (HL), A), resultando en 64 opcodes totales.

Lo que Falta Confirmar

  • Rendimiento: Verificar que la validación adicional no impacta significativamente el rendimiento del renderizado, especialmente en el bucle crítico de 160 píxeles por línea.
  • Casos Edge: Probar con tile IDs extremos (0, 127, 128, 255) en ambos modos de direccionamiento para asegurar que la validación cubre todos los casos posibles.

Hipótesis y Suposiciones

Asumimos que la validación con límites ajustados (VRAM_END - 15 y VRAM_END - 1) es suficiente para prevenir todos los accesos fuera de límites. Esto se basa en el conocimiento de que un tile completo son 16 bytes y una línea de tile son 2 bytes consecutivos.

Próximos Pasos

  • [ ] Recompilar el módulo C++ y ejecutar todos los tests para verificar que la corrección no rompe funcionalidad existente
  • [ ] Ejecutar el emulador con la ROM de Tetris para verificar que el renderizado funciona correctamente sin Segmentation Faults
  • [ ] Medir el rendimiento del renderizado para confirmar que la validación adicional no impacta significativamente el rendimiento
  • [ ] Documentar el hito de "fix completo del bug de renderizado" con capturas de pantalla si es posible