This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Implementation of the Window Layer
Summary
It was implementedWindow layerin the PPU to complete the graphical architecture of the emulator. The Window is an opaque layer that is drawn above the Background but below the Sprites, and is used for HUDs, Fixed markers and menus that should not move with the background scroll. In games like Tetris DX, the Window is used for the right column where the score, level and "Next Piece" are shown. The implementation includes controlling the WX (0xFF4B) and WY (0xFF4A) registers, as well as bits 5 and 6 of the LCDC register to enable the Window and select its tilemap. Test suite (4 tests) validating positioning, offset, enable bit and tilemap selection. All tests pass.
Hardware Concept
TheWindowis a special graphic layer of the Game Boy that is drawn on top of the Background but below the Sprites. Unlike the Background, the Window does not scroll: it remains fixed on the screen regardless of SCX/SCY values. This makes it ideal for user interfaces (HUDs), scoreboards, menus and other elements that must remain visible while the background scrolls.
The Window is controlled by several registers:
- LCDC Bit 5 (Window Enable):If it is 1, the Window is enabled and drawn. If it is 0, the Window is disabled and does not render, even if WX/WY are set.
- LCDC Bit 6 (Window Tile Map Area):Select the base tilemap for the Window:
- 0 = Tile Map at 0x9800
- 1 = Tile Map at 0x9C00
- WY (0xFF4A):Y position on screen where the window begins (0-143). If WY > 143, the Window is not drawn.
- WX (0xFF4B):X position on screen + 7 (historical offset). Yes WX< 7, la Window no se dibuja.
El píxel (x, y) está dentro de la Window si:
y >= WYandx + 7 >= WX.
The Window uses the same tile addressing mode as the Background (LCDC Bit 4): it can use unsigned (0x8000) or signed (0x8800). The Window also uses the same palette as the Background (BGP, 0xFF47).
Fountain:Pan Docs - LCD Control Register, Window
Implementation
Implemented Window logic insrc/gpu/renderer.pyinside the methodrender_frame().
The Window is rendered during the main pixel rendering loop, checking for each pixel whether it is
inside the Window region before drawing the Background pixel.
The implementation follows this flow:
- Read registers WX, WY and bits 5 and 6 of LCDC at start of
render_frame(). - Calculate the base Window tilemap based on LCDC bit 6 (0x9800 or 0x9C00).
- For each screen pixel (screen_x, screen_y):
- Check if the pixel is inside the Window region:
screen_y >= wyandscreen_x + 7 >= wx. - If inside and Window is enabled (bit 5 = 1):
- Calculate coordinates relative to the Window:
win_x = screen_x - (wx - 7),win_y = screen_y - wy. - Convert to tile coordinates in the Window tilemap.
- Read the Tile ID from the Window tilemap.
- Decode the tile pixel and draw it (overwriting the background).
- Calculate coordinates relative to the Window:
- If it is not inside or Window is disabled, draw the Background pixel (existing logic).
- Check if the pixel is inside the Window region:
Components created/modified
src/gpu/renderer.py: Importing IO_WX and IO_WY constants from mmu.pysrc/gpu/renderer.py: Reading WX, WY registers and LCDC bits inrender_frame()src/gpu/renderer.py: Window rendering logic inside pixel loopsrc/gpu/renderer.py: Update docstring and registry to include Window informationtests/test_gpu_window.py: Test suite for Window (4 tests)
Design decisions
- Integrated rendering:The Window is rendered within the same pixel loop as the Background, first checking if the pixel is within the Window region. This is efficient and keeps the code simple.
- Priority over Background:The Window always overwrites the Background when it is enabled and the pixel is within its region. This is the behavior of real hardware.
- Same addressing mode:The Window uses the same tile routing mode as the Background (LCDC Bit 4). This simplifies implementation and is correct according to the specification.
- Same palette:The Window uses the same palette as the Background (BGP). This is correct according to the specification for original Game Boy (DMG). On Game Boy Color, the Window can have its own palette, but that will be implemented later.
- Without internal scroll:The Window does not have an internal scroll. The Window's internal line counter (LY) is Will implement later if needed for specific games. For now, we use simple relative coordinates.
Affected Files
src/gpu/renderer.py- Implementation of Window logic in render_frame()tests/test_gpu_window.py- Test suite for Window (4 tests)
Tests and Verification
The test suite for Window was executed:
Command executed: pytest -q tests/test_gpu_window.py
Around:Windows 10, Python 3.13.5
Result: 4 passed, 2 warnings(3.49s)
What is valid:
- Window Positioning:Verify that with WX=7 (x=0) and WY=0, the Window covers the entire screen
- Window Offset:Verify that with WX=87 (x=80), the pixels on the left are from the background and those on the right are from the Window
- Enable bit:Verify that if LCDC Bit 5 (Window Enable) is 0, the Window is not drawn even if WX/WY are in range
- Tile Map Selection:Verify that LCDC bit 6 correctly selects the Window tilemap (0x9800 or 0x9C00)
Test code (example - test_window_positioning):
def test_window_positioning(self):
"""Verify that with WX=7 (x=0) and WY=0, the window covers the entire screen."""
mmu = MMU(None)
renderer = Renderer(mmu, scale=1)
# Configure 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)
# Set Window: WX=7 (x=0), WY=0
mmu.write_byte(IO_WX, 7)
mmu.write_byte(IO_WY, 0)
# Set Window tilemap: tile ID 1 (black) at position (0,0)
mmu.write_byte(0x9800, 0x01)
# Set tile to 0x8010 (tile ID 1 = black)
for line in range(8):
mmu.write_byte(0x8010 + (line * 2), 0xFF)
mmu.write_byte(0x8010 + (line * 2) + 1, 0xFF)
# Render frame
renderer.render_frame()
# Verify that the pixel (0,0) is black (Window tile color)
pixel_color = renderer.buffer.get_at((0, 0))
assert pixel_color == (0, 0, 0, 255)
Why this test demonstrates the behavior of the hardware:The test verifies that when WX=7 and WY=0, the Window covers the entire screen because WX=7 means that the Window starts at x=0 (due to the historical offset of 7 pixels). This is the exact behavior of real hardware according to Pan Docs. Additionally, it validates that the Window overwrites the Background when enabled.
Sources consulted
- Bread Docs:LCDC Register(bits 5 and 6: Window Enable and Window Tile Map Area)
- Bread Docs:Windows(WX, WY, rendering)
- Bread Docs:Memory Map(WX: 0xFF4B, WY: 0xFF4A)
Educational Integrity
What I Understand Now
- Window is a fixed layer:Unlike the Background, the Window does not scroll. remains fixed on the screen regardless of SCX/SCY. This makes it ideal for HUDs and menus.
- WX Historical Offset:WX has a historical offset of 7 pixels, which means WX=7 corresponds to x=0 on the screen. This is actual hardware behavior that is maintained for compatibility.
- Priority over Background:The Window always overwrites the Background when it is enabled and the pixel is within its region. This allows the Window to be used for interface elements that must be always visible.
- Standalone Tilemap:The Window can use a different tilemap than the Background (0x9800 or 0x9C00), which allows you to have different graphics for the Window and the Background.
- Same addressing mode:The Window uses the same tool addressing mode as the Background (LCDC Bit 4), which simplifies implementation.
What remains to be confirmed
- Internal line counter:The Window has an internal line counter (LY) that increments during rendering. For now, we use simple relative coordinates, but the internal counter may be necessary for specific games that use it for special effects.
- Independent palette in CGB:On Game Boy Color, the Window can have its own independent palette of the Background. This will be implemented later when full CGB support is added.
- Window out of bounds:Yes WX< 7 o WY >143, the Window is not drawn. This is implemented, but There may be edge cases that need more validation with real ROMs.
Hypotheses and Assumptions
For now, we assume that the Window has no internal scroll and that the internal line counter (LY) is not necessary for most games. If we find games that require this behavior, we will implement it later.
Next Steps
- [ ] Test with Tetris DX to verify that the right column (Score, Next Piece) renders correctly
- [ ] Implement Window's internal line counter (LY) if needed for specific games
- [ ] Add support for independent Window palette in CGB mode
- [ ] Optimize Window rendering if necessary (for now it is efficient as it is integrated into the main loop)