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
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:
- 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). - 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 enrender_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 enrender_scanline()para prevenir Segmentation Faults en modo signed addressing
Tests y Verificación
La corrección se validó mediante:
- Test existente:
test_signed_addressing_fixentests/test_core_ppu_rendering.pyverifica 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
grepque todos los 64 opcodes del bloque 0x80-0xBF estén implementados enCPU.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
- Pan Docs: Tile Data Addressing
- Pan Docs: LCD Control Register (LCDC)
- Pan Docs: CPU Instruction Set - ALU Operations
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