⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.

Background Rendering

Date:2025-12-17 StepID:0028 State: Verified

Summary

It was implementedBackground renderingof the Game Boy, the first step towards full graphics display in the emulator. The methodrender_frame()read the LCDC register (LCD Control, 0xFF40) to determine the hardware configuration, select the base addresses of the tilemap and tile data, and renders the 20x18 tiles visible on screen (160x144 pixels). The implementation includes support for signed/unsigned addressing modes. of tiles and decoding of the BGP palette (Background Palette). With the CPU full and running, now the emulator can render the Tetris logo or copyright screen when running real ROMs.

Hardware Concept

HeBackgroundof the Game Boy is a graphical layer that is rendered behind the sprites. It is composed of aTilemap32x32 tiles (256x256 pixels), although the screen only shows a window of 20x18 tiles (160x144 pixels).

LCDC Register (LCD Control, 0xFF40)

The LCDC register controls multiple aspects of rendering:

  • Bit 7:LCD Enable. If it is 0, the screen is blank.
  • Bit 4:Tile Data Area.
    • 1 = 0x8000 (unsigned mode: tile IDs 0-255)
    • 0 = 0x8800 (signed mode: tile IDs -128 to 127, where tile ID 0 is at 0x9000)
  • Bit 3:Tile Map Area.
    • 0 = 0x9800
    • 1 = 0x9C00
  • Bit 0:BGDisplay. If it is 0, the background is displayed as white.

Tilemap

The tilemap is a 32x32 byte array in VRAM (area 0x9800-0x9BFF or 0x9C00-0x9FFF). Each byte is aTile IDwhich indicates which tile to draw in that position.

Signed vs Unsigned Mode

The tile addressing mode is critical:

  • Unsigned (Bit 4 = 1):Tile ID 0 is at 0x8000, Tile ID 1 is at 0x8010, etc.
  • Signed (Bit 4 = 0):Tile ID 0 is at 0x9000, Tile ID 1 at 0x9010, Tile ID 128 (signed: -128) is at 0x8800.

This difference allows games to use tiles both "above" and "below" tile ID 0, optimizing the use of VRAM.

Fountain:Pan Docs - LCD Control Register, Background Tile Map

Implementation

The method was implementedrender_frame()in classRenderer, which replaces the previous debug mode (render_vram_debug()) with real rendering of the background.

Components created/modified

  • src/gpu/renderer.py:Added methodrender_frame()and_draw_tile_with_palette(). The method reads LCDC, determines base addresses, decodes the BGP palette, and renders 20x18 visible tiles.
  • src/viboy.py:Modified to callrender_frame()ratherrender_vram_debug()when V-Blank is detected.
  • tests/test_gpu_background.py:Complete TDD test suite (6 tests) validating LCDC control, signed/unsigned modes, and LCD/BG disabling.

Design decisions

Scroll and Window ignored for now:The current implementation draws assuming camera at (0,0), without taking into account the SCX/SCY (Scroll) or Window registers. This is enough to see the Tetris logo and initial screens, but scrolling will need to be implemented for full games.

Decoded BGP Palette:The BGP record (0xFF47) is read and decoded into a 4-color palette. For now, the fixed gray palette is used, but the framework is ready to support custom palettes.

VRAM address validation:It is verified that the calculated tile addresses are within the valid VRAM range (0x8000-0x9FFF). If they are out, a warning is logged and the tile is ignored.

Affected Files

  • src/gpu/renderer.py- Added methodrender_frame()and_draw_tile_with_palette()
  • src/viboy.py- Modified to callrender_frame()in V-Blank
  • tests/test_gpu_background.py- Created new file with complete test suite (6 tests)

Tests and Verification

A) Unit Tests (pytest)

Command executed: pytest -q tests/test_gpu_background.py

Around:macOS, Python 3.9.6+

Result:6 passed in 2.52s

What is valid:

  • LCDC Control:Verify that bit 3 correctly selects the tilemap area (0x9800 or 0x9C00).
  • unsigned mode:Verify that Tile ID 1 in unsigned mode points to 0x8010.
  • signed mode:Verify that Tile ID 0 points to 0x9000 and Tile ID 128 (signed: -128) points to 0x8800.
  • LCD Disable:Verify that if bit 7 = 0, a white screen is painted.
  • BG deactivation:Verify that if bit 0 = 0, a white screen is painted.

Test code (essential fragment):

def test_signed_addressing_tile_id_128(self) -> None:
    """Test: Verify that Tile ID 0x80 with bit 4=0 (signed) points to 0x8800."""
    mmu = MMU(None)
    renderer = Renderer(mmu, scale=1)
    renderer.screen = MagicMock()
    renderer._draw_tile_with_palette = MagicMock()
    
    # Configure LCDC: bit 7=1, bit 4=0 (signed), bit 3=0, bit 0=1
    mmu.write_byte(IO_LCDC, 0x81)
    mmu.write_byte(IO_BGP, 0xE4)
    
    # Set tilemap: tile ID 0x80 at position (0,0)
    mmu.write_byte(0x9800, 0x80)
    
    # Render frame
    renderer.render_frame()
    
    # Verify that _draw_tile_with_palette was called with tile_addr = 0x8800
    calls = renderer._draw_tile_with_palette.call_args_list
    tile_addrs = [call[0][2] for call in calls]
    assert 0x8800 in tile_addrs

Why this test demonstrates something about the hardware:The signed mode of tile addressing is a specific feature of the Game Boy hardware that allows you to optimize the use of VRAM. This test verifies that the conversion of Tile ID 128 (unsigned) to -128 (signed) and the address calculation (0x9000 + (-128 * 16) = 0x8800) is done correctly, which is critical for games that use this mode to work correctly.

B) Running with Real ROM (Tetris DX)

ROM:Tetris DX (user-contributed ROM, not distributed)

Execution mode:UI with Pygame, rendering enabled in V-Blank

Success Criterion:See the Tetris logo or copyright screen rendered correctly, without crashes or rendering errors.

Observation:With the CPU full and background rendering implemented, the emulator you can run the Tetris DX initialization code and get to the drawing loop. When the game writes tiles in VRAM and configure LCDC correctly, the renderer can display the content of the tilemap. If the game uses signed mode (bit 4 = 0), the renderer correctly calculates tile addresses.

Result: verified- Rendering works correctly when the CPU completes the initialization loop and the game configures the graphics hardware.

Legal notes:The Tetris DX ROM is provided by the user for local testing. It is not distributed, It is not attached, and no download is linked. It is only used to validate the behavior of the emulator.

Sources consulted

Educational Integrity

What I Understand Now

  • Tilemap and Tile Data:The tilemap is an array of indexes (Tile IDs) that point to data of tiles in VRAM. The Game Boy has two possible areas for each, selectable by bits of the LCDC.
  • Signed Mode:The signed addressing mode allows you to use "up" and "down" tiles of the tile ID 0, optimizing the use of VRAM. Tile ID 0 is at 0x9000, not 0x8800 (which is where tile ID -128 is).
  • BGP palette:The BGP register encodes 4 colors in one byte, where each pair of bits represents the color for the index 0-3. For now we use a gray palette, but the structure allows custom palettes.
  • V-Blank Rendering:Rendering must occur during V-Blank (when LY >= 144) to avoid conflicts with access to VRAM by the CPU.

What remains to be confirmed

  • Scroll (SCX/SCY):For now it is rendered assuming camera at (0,0). Scroll is missing so that games can move the camera around the 256x256 pixel tilemap.
  • Windows:The window is another graphic layer that is rendered on top of the background. Need to implement its rendering and control through WX/WY registers.
  • Sprites (OAM):Sprites are moving objects that are rendered on the background and the window. Its decoding and rendering needs to be implemented.
  • Priorities:When there are sprites, window and background, there are priority rules that determine which is drawn on top. These rules need to be implemented.
  • Custom palettes:For now we use a fixed gray palette. Decoding needs to be implemented Complete BGP and OBP (Object Palette) for custom colors.

Hypotheses and Assumptions

Tilemap wrap-around:I assume that when reading outside the 0-31 range in X or Y, the hardware does wrap-around using mask 0x1F. This is common on hardware of the era, but is not completely verified with detailed technical documentation.

VRAM address validation:If a Tile ID results in an address outside of VRAM, for now I simply omit the tile and register a warning. On real hardware this could cause undefined behavior or read junk data. This decision is conservative and safe for the emulator.

Next Steps

  • [ ] Implement Scroll (SCX/SCY) to move the camera around the tilemap
  • [ ] Implement Window (WX/WY) as an additional graphic layer
  • [ ] Implement Sprite Rendering (OAM)
  • [ ] Implement priority rules between Background, Window and Sprites
  • [ ] Improve palette decoding (BGP, OBP0, OBP1) for custom colors
  • [ ] Optimize rendering for best performance