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

Force Render and Scroll (SCX/SCY)

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

Summary

A was implemented"educational hack"to ignore Bit 0 of LCDC (BG Display) when Bit 7 (LCD Enable) is active, allowing CGB games like Tetris DX to writeLCDC=0x80can display graphics. Furthermore, it was implemented thescroll (SCX/SCY)which allows you to move the "camera" on the 256x256 pixel tilemap. The rendering was changed from drawing by tiles to drawing pixel by pixel to correctly support scrolling. They were created5 teststo validate scrolling and forced rendering all passing correctly.

Hardware Concept

HeLCDC register (0xFF40)It has different behaviors depending on the Game Boy model:

  • Game Boy Classic (DMG): Bit 0 (BG Display) directly controls whether the background is drawn or not. If Bit 0=0, the background is not rendered (white screen).
  • Game Boy Color (CGB): Bit 0 does not turn off the background, but rather changes thesprite priority vs background. The background is ALWAYS drawn in CGB (unless Master Priority is active).

Tetris DXIt is a CGB/dual game that detects the hardware and can writeLCDC=0x80(Bit 7=1, Bit 0=0) expecting CGB behavior where the background is drawn anyway. Our emulator acts as strict DMG and would shut down the background, resulting in a white screen.

TheScroll recordsThey allow you to move the "camera" on the tilemap:

  • SCX (0xFF43): Scroll X - horizontal scroll (0-255)
  • SCY (0xFF42): Scroll Y - vertical scroll (0-255)

The tilemap is32x32 tiles = 256x256 pixels. The screen shows only 160x144 pixels (20x18 tiles). The scroll allows you to "move the camera" over the entire tilemap. The formula is:

map_pixel_x = (screen_pixel_x + SCX) % 256
map_pixel_y = (screen_pixel_y + SCY) % 256

The wrap-around (module 256) allows the scrolling to be continuous and cyclical.

Source: Pan Docs - LCD Control Register, Game Boy Color differences, Scroll Registers (SCX/SCY)

Implementation

Educational Hack: Ignore LCDC Bit 0

LCDC Bit 0 verification commented onrender_frame()and documentation was added explaining that is a temporary hack for compatibility with CGB games. The original code is commented for future reference. Now, if Bit 7 (LCD Enable) is active, the background is always drawn, regardless of Bit 0.

Scroll Implementation (SCX/SCY)

Changed the rendering oftile by tiletopixel by pixelto properly support scrolling:

  • The SCX and SCY registers of the MMU are read
  • For each screen pixel (0-159, 0-143), the position in the tilemap is calculated by applying scroll
  • It is calculated which tile corresponds and which pixel within the tile
  • The specific pixel of the tile is decoded (reading the corresponding bits of the 2 bytes of the line)
  • The pixel is drawn at the screen position with the palette applied

The change from rendering by tiles to pixel by pixel is slower in Python, but allows scrolling to be implemented correctly and is more flexible for future enhancements (such as Window, Sprites, etc.).

Components created/modified

  • src/gpu/renderer.py: Modifiedrender_frame()to ignore Bit 0 and implement pixel-by-pixel scrolling
  • tests/test_gpu_scroll.py: New file with 5 tests to validate scrolling and forced rendering

Design decisions

The Bit 0 hack is clearly documented as temporary and educational. In the future, when implemented Full CGB mode, Bit 0 should function correctly according to the CGB specification.

Pixel-by-pixel rendering is slower but more correct and flexible. In the future, it could be optimized rendering by tiles when the scroll is a multiple of 8, but for now the pixel-by-pixel implementation is clearer and easier to maintain.

Affected Files

  • src/gpu/renderer.py- Modifiedrender_frame()to ignore Bit 0 of LCDC (educational hack) and implement scroll (SCX/SCY) with pixel-by-pixel rendering
  • tests/test_gpu_scroll.py- New file with 5 tests to validate horizontal, vertical scroll, wrap-around, forced rendering with LCDC=0x80, and zero scroll

Tests and Verification

Unit Tests - Scroll and Forced Rendering

Command executed: python3 -m pytest tests/test_gpu_scroll.py -v

Around: macOS (darwin 21.6.0), Python 3.9.6, pytest 8.4.2

Result: 5 passed in 11.81s

How valid:

  • test_scroll_x: Verifies that SCX correctly shifts the background horizontally. If SCX=4, screen pixel 0 should display tilemap pixel 4. Validate that the horizontal scroll works correctly.
  • test_scroll_y: Verifies that SCY correctly shifts the background vertically. If SCY=8, line 0 of the screen should show line 8 of the tilemap. Validate that the vertical scroll works correctly.
  • test_scroll_wrap_around: Verifies that the scroll does wrap-around correctly (module 256). If SCX=200 and screen_x=100, map_x = (100 + 200) % 256 = 44. Validates that the wrap-around works correctly.
  • test_force_bg_render_lcdc_0x80: Verify that with LCDC=0x80 (bit 7=1, bit 0=0) the background is drawn thanks to the educational hack. Validates that the hack allows CGB games to display graphics.
  • test_scroll_zero: Verify that with SCX=0 and SCY=0, rendering works normally without scrolling. Validates that rendering works correctly without scrolling.

Test Code - test_force_bg_render_lcdc_0x80

@patch('src.gpu.renderer.pygame.draw.rect')
def test_force_bg_render_lcdc_0x80(self, mock_draw_rect: MagicMock) -> None:
    """
    Test: Verify that with LCDC=0x80 (bit 7=1, bit 0=0) the background is drawn.
    
    This test validates the "educational hack" that ignores LCDC Bit 0 to
    allow CGB games (such as Tetris DX) that write LCDC=0x80 to
    show graphics.
    """
    mmu = MMU(None)
    renderer = Renderer(mmu, scale=1)
    renderer.screen = MagicMock()
    
    # Set LCDC = 0x80 (bit 7=1 LCD ON, bit 0=0 BG OFF in DMG)
    # With the hack, it should draw the background anyway
    mmu.write_byte(IO_LCDC, 0x80)
    mmu.write_byte(IO_BGP, 0xE4)
    
    # Configure basic tilemap
    mmu.write_byte(0x9800, 0x00)
    
    # Set tile to 0x8000 (tile ID 0)
    for line in range(8):
        mmu.write_byte(0x8000 + (line * 2), 0x00)
        mmu.write_byte(0x8000 + (line * 2) + 1, 0x00)
    
    # Render frame
    renderer.render_frame()
    
    # Verify that draw_rect was called (indicating pixels were drawn)
    assert mock_draw_rect.called, \
        "With LCDC=0x80 (educational hack), you should draw pixels instead of returning early"
    assert mock_draw_rect.call_count == 160 * 144, \
        f"It should draw 160*144 pixels, but {mock_draw_rect.call_count} was called times"
    
    renderer.quit()

Academic explanation: This test shows that the educational hack works correctly. With LCDC=0x80, the renderer should return early without drawing (strict DMG behavior), but thanks to the hack, go ahead and draw the 160*144 pixels of the screen. This allows CGB games like Tetris DX that write LCDC=0x80 can display graphics in our emulator.

Sources consulted

Educational Integrity

What I Understand Now

  • LCDC bit 0 in CGB: On Game Boy Color, Bit 0 does not turn off the background, but instead changes the priority of sprites vs background. The background is always drawn in CGB (unless there is Master Priority).
  • Scroll (SCX/SCY): Scroll registers allow the "camera" to be moved over the 256x256 pixel tilemap. The formula ismap_pixel = (screen_pixel + scroll) % 256with wrap-around.
  • Pixel-by-pixel rendering: To implement scroll correctly, it is necessary to render pixel by pixel instead of tile by tile, since scroll can move the camera to any position (not just multiples of 8).

What remains to be confirmed

  • Exact behavior of Bit 0 in CGB: I need to check the exact specification of how Bit 0 works in CGB and when Master Priority is applied. This will be important when we implement full CGB mode.
  • Rendering optimization: Pixel-by-pixel rendering is slower. In the future, it could be optimized by rendering by tiles when the scroll is a multiple of 8, but for now the pixel-by-pixel implementation is clearer.

Hypotheses and Assumptions

The Bit 0 hack is aeducational assumptionbased on the diagnosis that Tetris DX writes LCDC=0x80 waiting for the background to be drawn. I have not fully verified the CGB specification of Bit 0, but the hack allows the game to display graphics, which is the immediate goal. In the future, when Let's implement full CGB mode, Bit 0 should work correctly according to the specification.

Next Steps

  • [ ] Try Tetris DX with the Bit 0 hack and verify that graphics are displayed
  • [ ] Verify that scrolling works correctly in the game (animations, background scrolling)
  • [ ] Implement Window (WX/WY) to support overlapping windows
  • [ ] Implement Sprites (OAM) to render moving objects
  • [ ] Investigate exact specification of Bit 0 in CGB to implement full CGB mode