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

Implementación de la Capa Window (Ventana)

Fecha: 2025-12-18 Step ID: 0039 Estado: Verified

Resumen

Se implementó la capa Window (Ventana) en la PPU para completar la arquitectura gráfica del emulador. La Window es una capa opaca que se dibuja encima del Background pero debajo de los Sprites, y se usa para HUDs, marcadores y menús fijos que no deben moverse con el scroll del fondo. En juegos como Tetris DX, la Window se usa para la columna derecha donde se muestra la puntuación, el nivel y la "Siguiente Pieza". La implementación incluye el control de los registros WX (0xFF4B) y WY (0xFF4A), así como los bits 5 y 6 del registro LCDC para habilitar la Window y seleccionar su tilemap. Suite de tests (4 tests) validando posicionamiento, offset, enable bit y selección de tilemap. Todos los tests pasan.

Concepto de Hardware

La Window (Ventana) es una capa gráfica especial de la Game Boy que se dibuja encima del Background pero debajo de los Sprites. A diferencia del Background, la Window no hace scroll: permanece fija en la pantalla independientemente de los valores de SCX/SCY. Esto la hace ideal para interfaces de usuario (HUDs), marcadores, menús y otros elementos que deben permanecer visibles mientras el fondo se desplaza.

La Window se controla mediante varios registros:

  • LCDC Bit 5 (Window Enable): Si es 1, la Window está habilitada y se dibuja. Si es 0, la Window está deshabilitada y no se renderiza, incluso si WX/WY están configurados.
  • LCDC Bit 6 (Window Tile Map Area): Selecciona el tilemap base para la Window:
    • 0 = Tile Map en 0x9800
    • 1 = Tile Map en 0x9C00
  • WY (0xFF4A): Posición Y en pantalla donde empieza la ventana (0-143). Si WY > 143, la Window no se dibuja.
  • WX (0xFF4B): Posición X en pantalla + 7 (offset histórico). Si WX < 7, la Window no se dibuja. El píxel (x, y) está dentro de la Window si: y >= WY y x + 7 >= WX.

La Window usa el mismo modo de direccionamiento de tiles que el Background (LCDC Bit 4): puede usar direccionamiento unsigned (0x8000) o signed (0x8800). La Window también usa la misma paleta que el Background (BGP, 0xFF47).

Fuente: Pan Docs - LCD Control Register, Window

Implementación

Se implementó la lógica de Window en src/gpu/renderer.py dentro del método render_frame(). La Window se renderiza durante el bucle principal de renderizado de píxeles, verificando para cada píxel si está dentro de la región de Window antes de dibujar el píxel del Background.

La implementación sigue este flujo:

  1. Leer los registros WX, WY y los bits 5 y 6 de LCDC al inicio de render_frame().
  2. Calcular el tilemap base de Window según el bit 6 de LCDC (0x9800 o 0x9C00).
  3. Para cada píxel de pantalla (screen_x, screen_y):
    • Verificar si el píxel está dentro de la región de Window: screen_y >= wy y screen_x + 7 >= wx.
    • Si está dentro y Window está habilitada (bit 5 = 1):
      • Calcular coordenadas relativas a la Window: win_x = screen_x - (wx - 7), win_y = screen_y - wy.
      • Convertir a coordenadas de tile en el tilemap de Window.
      • Leer el Tile ID del tilemap de Window.
      • Decodificar el píxel del tile y dibujarlo (sobrescribiendo el fondo).
    • Si no está dentro o Window está deshabilitada, dibujar el píxel del Background (lógica existente).

Componentes creados/modificados

  • src/gpu/renderer.py: Importación de constantes IO_WX e IO_WY desde mmu.py
  • src/gpu/renderer.py: Lectura de registros WX, WY y bits de LCDC en render_frame()
  • src/gpu/renderer.py: Lógica de renderizado de Window dentro del bucle de píxeles
  • src/gpu/renderer.py: Actualización del docstring y logging para incluir información de Window
  • tests/test_gpu_window.py: Suite de tests para Window (4 tests)

Decisiones de diseño

  • Renderizado integrado: La Window se renderiza dentro del mismo bucle de píxeles que el Background, verificando primero si el píxel está dentro de la región de Window. Esto es eficiente y mantiene el código simple.
  • Prioridad sobre Background: La Window siempre sobrescribe el Background cuando está habilitada y el píxel está dentro de su región. Esto es el comportamiento del hardware real.
  • Mismo modo de direccionamiento: La Window usa el mismo modo de direccionamiento de tiles que el Background (LCDC Bit 4). Esto simplifica la implementación y es correcto según la especificación.
  • Misma paleta: La Window usa la misma paleta que el Background (BGP). Esto es correcto según la especificación para Game Boy original (DMG). En Game Boy Color, la Window puede tener su propia paleta, pero eso se implementará más adelante.
  • Sin scroll interno: La Window no tiene scroll interno. El contador de líneas interno de la Window (LY) se implementará más adelante si es necesario para juegos específicos. Por ahora, usamos coordenadas relativas simples.

Archivos Afectados

  • src/gpu/renderer.py - Implementación de lógica de Window en render_frame()
  • tests/test_gpu_window.py - Suite de tests para Window (4 tests)

Tests y Verificación

Se ejecutó la suite de tests para Window:

Comando ejecutado: pytest -q tests/test_gpu_window.py

Entorno: Windows 10, Python 3.13.5

Resultado: 4 passed, 2 warnings (3.49s)

Qué valida:

  • Posicionamiento de Window: Verifica que con WX=7 (x=0) y WY=0, la Window cubre toda la pantalla
  • Offset de Window: Verifica que con WX=87 (x=80), los píxeles a la izquierda son del fondo y los de la derecha son de la Window
  • Bit de Enable: Verifica que si LCDC Bit 5 (Window Enable) es 0, no se dibuja la Window aunque WX/WY estén en rango
  • Selección de Tile Map: Verifica que el bit 6 de LCDC selecciona correctamente el tilemap de Window (0x9800 o 0x9C00)

Código del test (ejemplo - test_window_positioning):

def test_window_positioning(self):
    """Verifica que con WX=7 (x=0) y WY=0, la ventana cubre toda la pantalla."""
    mmu = MMU(None)
    renderer = Renderer(mmu, scale=1)
    
    # Configurar LCDC: bit 7=1, bit 5=1 (Window Enable), bit 4=1, bit 3=0, bit 0=1
    mmu.write_byte(IO_LCDC, 0xB1)  # 10110001
    mmu.write_byte(IO_BGP, 0xE4)
    
    # Configurar Window: WX=7 (x=0), WY=0
    mmu.write_byte(IO_WX, 7)
    mmu.write_byte(IO_WY, 0)
    
    # Configurar tilemap de Window: tile ID 1 (negro) en posición (0,0)
    mmu.write_byte(0x9800, 0x01)
    
    # Configurar tile en 0x8010 (tile ID 1 = negro)
    for line in range(8):
        mmu.write_byte(0x8010 + (line * 2), 0xFF)
        mmu.write_byte(0x8010 + (line * 2) + 1, 0xFF)
    
    # Renderizar frame
    renderer.render_frame()
    
    # Verificar que el píxel (0,0) es negro (color del tile de Window)
    pixel_color = renderer.buffer.get_at((0, 0))
    assert pixel_color == (0, 0, 0, 255)

Por qué este test demuestra el comportamiento del hardware: El test verifica que cuando WX=7 y WY=0, la Window cubre toda la pantalla porque WX=7 significa que la Window comienza en x=0 (debido al offset histórico de 7 píxeles). Esto es el comportamiento exacto del hardware real según Pan Docs. Además, valida que la Window sobrescribe el Background cuando está habilitada.

Fuentes Consultadas

  • Pan Docs: LCDC Register (bits 5 y 6: Window Enable y Window Tile Map Area)
  • Pan Docs: Window (WX, WY, renderizado)
  • Pan Docs: Memory Map (WX: 0xFF4B, WY: 0xFF4A)

Integridad Educativa

Lo que Entiendo Ahora

  • Window es una capa fija: A diferencia del Background, la Window no hace scroll. Permanece fija en la pantalla independientemente de SCX/SCY. Esto la hace ideal para HUDs y menús.
  • Offset histórico de WX: WX tiene un offset histórico de 7 píxeles, lo que significa que WX=7 corresponde a x=0 en pantalla. Esto es un comportamiento del hardware real que se mantiene por compatibilidad.
  • Prioridad sobre Background: La Window siempre sobrescribe el Background cuando está habilitada y el píxel está dentro de su región. Esto permite que la Window se use para elementos de interfaz que deben estar siempre visibles.
  • Tilemap independiente: La Window puede usar un tilemap diferente al Background (0x9800 o 0x9C00), lo que permite tener gráficos diferentes para la Window y el Background.
  • Mismo modo de direccionamiento: La Window usa el mismo modo de direccionamiento de tiles que el Background (LCDC Bit 4), lo que simplifica la implementación.

Lo que Falta Confirmar

  • Contador de líneas interno: La Window tiene un contador de líneas interno (LY) que se incrementa durante el renderizado. Por ahora, usamos coordenadas relativas simples, pero el contador interno puede ser necesario para juegos específicos que lo usan para efectos especiales.
  • Paleta independiente en CGB: En Game Boy Color, la Window puede tener su propia paleta independiente del Background. Esto se implementará más adelante cuando se añada soporte completo para CGB.
  • Window fuera de límites: Si WX < 7 o WY > 143, la Window no se dibuja. Esto está implementado, pero puede haber casos edge que necesiten más validación con ROMs reales.

Hipótesis y Suposiciones

Por ahora, asumimos que la Window no tiene scroll interno y que el contador de líneas interno (LY) no es necesario para la mayoría de los juegos. Si encontramos juegos que requieren este comportamiento, lo implementaremos más adelante.

Próximos Pasos

  • [ ] Probar con Tetris DX para verificar que la columna derecha (Score, Next Piece) se renderiza correctamente
  • [ ] Implementar contador de líneas interno de Window (LY) si es necesario para juegos específicos
  • [ ] Añadir soporte para paleta independiente de Window en modo CGB
  • [ ] Optimizar el renderizado de Window si es necesario (por ahora es eficiente al estar integrado en el bucle principal)