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.
Arquitectura Gráfica: Sincronización del Framebuffer con V-Blank
Resumen
El diagnóstico del Step 0199 confirmó una condición de carrera: el framebuffer se limpia desde Python antes de que la PPU tenga tiempo de dibujar, resultando en una pantalla blanca. Aunque el primer fotograma (el logo de Nintendo) se renderiza correctamente, los fotogramas posteriores se muestran en blanco porque la limpieza ocurre asíncronamente al hardware emulado.
Este Step resuelve el problema arquitectónicamente: la responsabilidad de limpiar el framebuffer se mueve de Python a C++, activándose precisamente cuando la PPU inicia el renderizado de un nuevo fotograma (cuando LY se resetea a 0). Esta sincronización elimina la condición de carrera y garantiza que el framebuffer esté siempre limpio justo antes de que el primer píxel del nuevo fotograma sea dibujado.
Concepto de Hardware: Sincronización con el Barrido Vertical (V-Sync)
El ciclo de renderizado de la Game Boy es inmutable. La PPU dibuja 144 líneas visibles (LY 0-143) y luego entra en el período de V-Blank (LY 144-153). Cuando el ciclo termina, LY se resetea a 0 para comenzar el siguiente fotograma. Este momento, el cambio de LY a 0, es el "pulso" de sincronización vertical (V-Sync) del hardware. Es el punto de partida garantizado para cualquier operación de renderizado de un nuevo fotograma.
Al anclar nuestra lógica de clear_framebuffer() a este evento, eliminamos la condición de carrera. La limpieza ocurrirá dentro del mismo "tick" de hardware que inicia el dibujo, garantizando que el lienzo esté siempre limpio justo antes de que el primer píxel del nuevo fotograma sea dibujado, pero nunca antes.
La Condición de Carrera del Step 0199:
- Frame 0: Python llama a
clear_framebuffer()→ El buffer C++ se llena de ceros → La CPU ejecuta ~17,556 instrucciones → La ROM estableceLCDC=0x91→ La PPU renderiza el logo de Nintendo → Python muestra el logo (visible por 1/60s). - Frame 1: Python llama a
clear_framebuffer()→ El buffer C++ se borra inmediatamente → La CPU ejecuta instrucciones → El juego estableceLCDC=0x80(fondo apagado) → La PPU no dibuja nada → Python lee el framebuffer (lleno de ceros) → Pantalla blanca.
La Solución Arquitectónica: La responsabilidad de limpiar el framebuffer no debe ser del bucle principal de Python (que es asíncrono al hardware), sino del propio hardware emulado. La PPU debe limpiar su propio lienzo justo cuando está a punto de empezar a dibujar un nuevo fotograma. ¿Y cuándo ocurre eso? Exactamente cuando la línea de escaneo (LY) vuelve a ser 0.
Implementación
Este Step mueve la lógica de limpieza del framebuffer desde el orquestador de Python a la PPU de C++, sincronizándola con el reseteo de LY a 0.
1. Modificación en PPU::step() (C++)
En src/core/cpp/PPU.cpp, dentro del método step(), añadimos la llamada a clear_framebuffer() justo cuando ly_ se resetea a 0:
// Si pasamos la última línea (153), reiniciar a 0 (nuevo frame)
if (ly_ > 153) {
ly_ = 0;
// Reiniciar flag de interrupción STAT al cambiar de frame
stat_interrupt_line_ = 0;
// --- Step 0200: Limpieza Sincrónica del Framebuffer ---
// Limpiar el framebuffer justo cuando empieza el nuevo fotograma (LY=0).
// Esto elimina la condición de carrera: la limpieza ocurre dentro del mismo
// "tick" de hardware que inicia el dibujo, garantizando que el lienzo esté
// siempre limpio justo antes de que el primer píxel del nuevo fotograma sea dibujado.
clear_framebuffer();
}
Esta modificación garantiza que:
- La limpieza ocurre dentro del mismo ciclo de hardware que inicia el nuevo fotograma.
- No hay condición de carrera: la PPU controla su propio framebuffer.
- El framebuffer está limpio justo antes de que la primera línea visible (LY=0) comience a renderizarse.
2. Eliminación de la Limpieza Asíncrona en Python
En src/viboy.py, eliminamos la llamada a clear_framebuffer() del bucle principal:
# Bucle principal del emulador
while self.running:
# --- Step 0200: La limpieza del framebuffer ahora es responsabilidad de la PPU ---
# La PPU limpia el framebuffer sincrónicamente cuando LY se resetea a 0,
# eliminando la condición de carrera entre Python y C++.
# --- Bucle de Frame Completo (154 scanlines) ---
for line in range(SCANLINES_PER_FRAME):
# ... resto del bucle ...
El orquestador de Python ya no es responsable de la limpieza. Esta responsabilidad pertenece exclusivamente a la PPU, que conoce el timing exacto del hardware.
3. Integración del Logo Personalizado "VIBOY COLOR"
Como parte de este Step, también integramos el logo personalizado "VIBOY COLOR" en lugar del logo estándar de Nintendo. Para facilitar esta tarea, creamos un script de conversión automática que transforma una imagen PNG en el array de 48 bytes requerido por el formato del header del cartucho.
3.1. Script de Conversión de Logo
Se creó el script tools/logo_converter/convert_logo_to_header.py para convertir automáticamente imágenes PNG al formato de header de cartucho de Game Boy. El script está documentado en tools/logo_converter/README.md y está disponible en GitHub para que otros desarrolladores puedan usarlo.
Código completo del script:
#!/usr/bin/env python3
"""
Script para convertir una imagen PNG a formato de header de cartucho de Game Boy.
El logo de Nintendo en el header del cartucho (0x0104-0x0133) son 48 bytes
que representan 48x8 píxeles en formato 1-bit (1 bit por píxel).
Formato:
- 48 bytes = 48 columnas x 8 filas
- Cada byte representa 8 píxeles verticales (1 bit por píxel)
- Bit 7 = píxel superior, Bit 0 = píxel inferior
- 0 = blanco/transparente, 1 = negro/visible
Fuente: Pan Docs - "Nintendo Logo", Cart Header (0x0104-0x0133)
"""
from PIL import Image
import sys
from pathlib import Path
def image_to_gb_logo_header(image_path: str, output_cpp: bool = True) -> str:
"""
Convierte una imagen PNG a un array de 48 bytes para el header del cartucho.
Args:
image_path: Ruta a la imagen PNG
output_cpp: Si es True, genera código C++. Si False, solo muestra los bytes.
Returns:
String con el código C++ o los bytes en formato hexadecimal
"""
try:
# Abrir la imagen
img = Image.open(image_path)
print(f"Imagen original cargada: {img.size} píxeles, modo: {img.mode}")
# Redimensionar a 48x8 (ancho x alto)
# Usamos LANCZOS para mejor calidad en el downscale
img_resized = img.resize((48, 8), Image.Resampling.LANCZOS)
print(f"Imagen redimensionada: {img_resized.size} píxeles")
# Convertir a escala de grises si no lo está
if img_resized.mode != 'L':
img_gray = img_resized.convert('L')
else:
img_gray = img_resized
# Convertir a 1-bit (blanco y negro) usando umbral
# Umbral: píxeles más oscuros que 128 se convierten a negro (1),
# píxeles más claros a blanco (0)
img_1bit = img_gray.point(lambda x: 0 if x > 128 else 255, mode='1')
# Guardar versión de referencia (opcional, para debugging)
debug_path = Path(image_path).parent / "viboy_logo_48x8_debug.png"
img_1bit.save(debug_path)
print(f"Versión 48x8 guardada en: {debug_path}")
# Obtener los píxeles como lista
pixels = list(img_1bit.getdata())
# El formato del header es:
# - 48 bytes = 48 columnas
# - Cada byte representa 8 píxeles verticales (1 bit por píxel)
# - Bit 7 = píxel superior (fila 0), Bit 0 = píxel inferior (fila 7)
# - 0 en PIL '1' mode = negro (255), 1 = blanco (0)
# - En Game Boy: 1 = visible/negro, 0 = transparente/blanco
header_data = bytearray(48)
# Para cada columna (0-47)
for col in range(48):
byte_value = 0
# Para cada fila (0-7), desde arriba hacia abajo
for row in range(8):
# Calcular índice del píxel en la lista plana
pixel_index = row * 48 + col
if pixel_index < len(pixels):
# En modo '1' de PIL: 0 = negro, 255 = blanco
# Pero en realidad, getdata() devuelve 0 para negro y 255 para blanco
# Necesitamos invertir: si el píxel es negro (0), poner el bit a 1
pixel_value = pixels[pixel_index]
if pixel_value == 0: # Negro en PIL
# Bit 7-row: bit más significativo para la fila superior
byte_value |= (1 << (7 - row))
header_data[col] = byte_value
# Formatear para C++
if output_cpp:
cpp_array = "// --- Logo Personalizado 'Viboy Color' (48x8 píxeles, formato 1bpp) ---\n"
cpp_array += "// Convertido desde: " + str(Path(image_path).name) + "\n"
cpp_array += "// Formato: 48 bytes = 48 columnas x 8 filas (1 bit por píxel)\n"
cpp_array += "// Bit 7 = píxel superior, Bit 0 = píxel inferior\n"
cpp_array += "// 1 = visible/negro, 0 = transparente/blanco\n"
cpp_array += "static const uint8_t VIBOY_LOGO_HEADER_DATA[48] = {\n "
for i, byte in enumerate(header_data):
cpp_array += f"0x{byte:02X}, "
if (i + 1) % 12 == 0:
cpp_array += "\n "
cpp_array = cpp_array.rstrip(", \n ") + "\n};"
return cpp_array
else:
# Solo mostrar los bytes en formato hexadecimal
hex_string = " ".join(f"{b:02X}" for b in header_data)
return hex_string
except FileNotFoundError:
return f"Error: No se encontró el archivo en la ruta: {image_path}"
except Exception as e:
return f"Error al procesar la imagen: {e}"
if __name__ == "__main__":
# Ruta por defecto
default_path = "assets/svg viboycolor logo.png"
# Permitir pasar la ruta como argumento
if len(sys.argv) > 1:
image_path = sys.argv[1]
else:
image_path = default_path
# Verificar que el archivo existe
if not Path(image_path).exists():
print(f"Error: No se encontró el archivo: {image_path}")
print(f"Buscando en: {Path(image_path).absolute()}")
sys.exit(1)
# Convertir
print(f"Convirtiendo: {image_path}")
print("-" * 60)
result = image_to_gb_logo_header(image_path, output_cpp=True)
print("\n" + "=" * 60)
print("ARRAY C++ GENERADO:")
print("=" * 60)
print(result)
print("=" * 60)
# Guardar también en un archivo
output_file = Path("tools") / "viboy_logo_header.txt"
with open(output_file, "w", encoding="utf-8") as f:
f.write(result)
print(f"\nArray guardado también en: {output_file}")
3.2. Uso del Script
El script se ejecuta desde la línea de comandos:
# Usar la ruta por defecto (assets/svg viboycolor logo.png)
python tools/logo_converter/convert_logo_to_header.py
# O especificar una imagen personalizada
python tools/logo_converter/convert_logo_to_header.py ruta/a/tu/imagen.png
El script genera:
- Un array C++ listo para usar en
MMU.cpp - Un archivo de texto con el array en
tools/viboy_logo_header.txt - Una imagen de debug en
assets/viboy_logo_48x8_debug.pngpara verificación visual
3.3. Array Generado
El array final generado desde la imagen assets/svg viboycolor logo.png es:
// --- Step 0200: Datos del Logo Personalizado "Viboy Color" (Post-BIOS) ---
// La Boot ROM copia los datos del logo desde el encabezado del cartucho (0x0104-0x0133)
// a la VRAM. Estos son los 48 bytes del logo personalizado "VIBOY COLOR" convertidos
// desde una imagen de 48x8 píxeles a formato de header de cartucho (1bpp).
//
// Convertido desde: assets/svg viboycolor logo.png
// Formato: 48 bytes = 48 columnas x 8 filas (1 bit por píxel)
// Bit 7 = píxel superior, Bit 0 = píxel inferior
// 1 = visible/negro, 0 = transparente/blanco
//
// Fuente: Pan Docs - "Nintendo Logo", Cart Header (0x0104-0x0133)
// El logo de Nintendo original se usa como mecanismo antipiratería: la Boot ROM
// compara estos bytes con los del encabezado del cartucho. Si no coinciden, el
// sistema se congela. En nuestro caso, usamos un logo personalizado.
//
// NOTA: Este array fue generado automáticamente usando tools/convert_logo_to_header.py
static const uint8_t VIBOY_LOGO_HEADER_DATA[48] = {
0xF7, 0xC3, 0x9D, 0xBD, 0xBE, 0x7E, 0x6E, 0x76, 0x66, 0x7E, 0x66, 0x7E,
0x66, 0x66, 0x7E, 0x66, 0x7E, 0x6E, 0x66, 0x6E, 0x66, 0x6E, 0x7E, 0x7E,
0x66, 0x6E, 0x7E, 0x7E, 0x66, 0x7E, 0x66, 0x7E, 0x66, 0x7E, 0x7E, 0x66,
0x7E, 0x6E, 0x76, 0x66, 0x66, 0x66, 0x7E, 0xBE, 0xBD, 0x9D, 0xC3, 0xE7
};
Nota sobre el Mecanismo Antipiratería de Nintendo: El logo de Nintendo en el encabezado del cartucho (0x0104-0x0133) no es solo decorativo. La Boot ROM oficial compara estos 48 bytes con los datos que copia a la VRAM. Si no coinciden, el sistema se congela, impidiendo que juegos no autorizados se ejecuten. Este es uno de los primeros mecanismos antipiratería de la industria de los videojuegos.
Disponibilidad en GitHub: El script está disponible en el directorio tools/logo_converter/ del repositorio, junto con documentación completa en README.md, para que otros desarrolladores puedan usarlo para personalizar sus propios emuladores o proyectos relacionados con Game Boy.
Archivos Afectados
src/core/cpp/PPU.cpp- Añadida llamada aclear_framebuffer()cuandoly_se resetea a 0src/viboy.py- Eliminada llamada asíncrona aclear_framebuffer()del bucle principalsrc/core/cpp/MMU.cpp- ReemplazadoNINTENDO_LOGO_DATAconVIBOY_LOGO_HEADER_DATAgenerado desde la imagentools/logo_converter/convert_logo_to_header.py- Script de conversión de imágenes PNG a formato header de cartucho (NUEVO)tools/logo_converter/README.md- Documentación completa del script (NUEVO)README.md- Añadida sección de herramientas y utilidades con mención al Logo Converter (NUEVO)docs/bitacora/entries/2025-12-21__0200__arquitectura-grafica-sincronizacion-framebuffer-vblank.html- Nueva entrada de bitácoradocs/bitacora/index.html- Actualizado con la nueva entradaINFORME_FASE_2.md- Actualizado con el Step 0200
Tests y Verificación
La validación de este cambio es visual y funcional:
-
Recompilación del módulo C++:
python setup.py build_ext --inplace # O usando el script de PowerShell: .\rebuild_cpp.ps1 -
Ejecución del emulador:
python main.py roms/tetris.gb -
Resultado Esperado:
- El logo de Nintendo (o el logo personalizado "VIBOY COLOR") se muestra de forma estable durante aproximadamente un segundo.
- Cuando el juego establece
LCDC=0x80(fondo apagado), la pantalla se vuelve blanca de forma limpia, sin artefactos "fantasma". - No hay condición de carrera: el framebuffer se limpia sincrónicamente con el inicio de cada fotograma.
Validación de módulo compilado C++: Este cambio modifica el comportamiento del bucle de emulación en C++, por lo que es crítico verificar que la compilación se complete sin errores y que el emulador funcione correctamente.
Conclusión
Este Step resuelve definitivamente la condición de carrera del framebuffer moviendo la responsabilidad de la limpieza desde el orquestador de Python (asíncrono) a la PPU de C++ (sincrónica con el hardware). Al anclar la limpieza al evento de reseteo de LY a 0, garantizamos que el framebuffer esté siempre limpio justo antes de que el primer píxel del nuevo fotograma sea dibujado, pero nunca antes.
Esta solución arquitectónica es más robusta y precisa que la anterior, ya que respeta el timing exacto del hardware emulado. El resultado es un ciclo de renderizado estable y preciso, sin artefactos visuales ni condiciones de carrera.