This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Fix BG Tilemap Base + Scroll Diagnostics + Fix
Summary
Diagnosis and correction of the problem of white screens/scrolling patterns, caused by incorrect selection of base BG tilemap (LCDC bit3) and/or incorrect application of scroll (SCX/SCY). Added instrumentation to diagnose which tilemap has data and which one is being used, fixed use ofMMU::read_vram()to read tilemap (instead ofread()direct), and clean-room tests were created to validate the selection of base and scroll tilemap.
Hardware Concept
The Game Boy has two background tilemaps (BG tilemap) that can be selected based on bit 3 of the LCDC register (LCDC bit3):
- LCDC bit3=0: BG tilemap base on 0x9800-0x9BFF (32x32 tiles = 1024 bytes)
- LCDC bit3=1: BG tilemap base on 0x9C00-0x9FFF (32x32 tiles = 1024 bytes)
Scroll (SCX/SCY): Registers SCX (0xFF43) and SCY (0xFF42) control the background offset. The scroll is applied with wrap 0..255, that is:
map_x = (x + scx) & 0xFFmap_y = (ly + scy) & 0xFF
The problem: If the emulator uses the wrong tilemap or incorrectly reads the tilemap (usingread()ratherread_vram()), may render incorrect or empty tiles, resulting in white screens or incorrectly scrolling patterns.
Reference: Pan Docs - LCD Control Register (LCDC), bit 3: BG Tile Map Display Select. Pan Docs - Scroll Registers (SCX/SCY).
Implementation
The fix was implemented in four phases:
Phase A: Minimum Instrumentation
Added logging to diagnose which tilemap has data and which one is being used:
tools/rom_smoke_0442.py: Added nonzero byte count in both tilemaps (0x9800 and 0x9C00) and displays 16 tile IDs from the current basesrc/core/cpp/PPU.cpp: Added log[PPU-TILEMAP-DIAG]showing LCDC, bg_map_base, tilemap_nz_9800, tilemap_nz_9C00, and tile_ids_sample (first 5 frames + every 120)src/viboy.py: Added log[ENV]at boot for evidence of kill-switches OFF
Phase B: Fix Core
Fixed the use ofMMU::read_vram()to read tilemap insteadread()straight:
// BEFORE (line 2748):
uint8_t tile_id = mmu_->read(tile_map_addr);
// AFTER:
uint16_t tile_map_offset = tile_map_addr - 0x8000;
uint8_t tile_id = 0x00;
if (tile_map_offset< 0x2000) { // Rango válido VRAM
tile_id = mmu_->read_vram(tile_map_offset);
}
Also fixed usage in immediate tilemap check (line 2255).
Phase C: Clean-Room Tests
Tests were created intests/test_bg_tilemap_base_and_scroll_0464.py:
- test_tilemap_base_select_9800: Verify that with LCDC bit3=0 tilemap 0x9800 is used
- test_tilemap_base_select_9C00: Verify that with LCDC bit3=1 tilemap 0x9C00 is used
- test_scx_scroll: Verifies that SCX is applied correctly (placeholder for framebuffer verification)
Phase D: Actual Validation
The actual validation with grid UI will be done in a later step. The diagnostic logs will allow you to identify if the problem is in the base tilemap, scroll, or in other components (Window, OBJ, CGB attrs).
Affected Files
src/core/cpp/PPU.cpp- Added logging [PPU-TILEMAP-DIAG] and fixed use of read_vram() to read tilemap (lines 2171-2221, 2748, 2255)tools/rom_smoke_0442.py- Added counting of nonzero bytes in tilemaps and display of tile IDs (lines 374-409, 512-516)src/viboy.py- Added log [ENV] to boot for evidence of kill-switches (lines 689-704)tests/test_bg_tilemap_base_and_scroll_0464.py- New clean-room tests (3 tests)
Tests and Verification
Command executed: pytest tests/test_bg_tilemap_base_and_scroll_0464.py -v
Result: ✅ 3/3 tests pass
Test Code:
def test_tilemap_base_select_9800(self):
"""Test 1: tilemap base select (0x9800 vs 0x9C00) - Case 0x9800."""
# Write tile 0 at 0x8000 (pattern 0x55/0x33)
for line in range(8):
self.mmu.write(0x8000 + (line * 2), 0x55)
self.mmu.write(0x8000 + (line * 2) + 1, 0x33)
# Set to 0x9800: tile IDs = 0
for i in range(32 * 32):
self.mmu.write(0x9800 + i, 0x00)
# Set to 0x9C00: tile IDs = 1
for i in range(32 * 32):
self.mmu.write(0x9C00 + i, 0x01)
# Set LCDC bit3=0 (tilemap base 0x9800)
self.mmu.write(0xFF40, 0x91) # Bit3=0 → 0x9800
# Run 1 frame and verify that tile 0 is read
Native Validation: Tests validate compiled C++ module (viboy_core).
Sources consulted
- Pan Docs: LCD Control Register (LCDC), bit 3: BG Tile Map Display Select
- Pan Docs: Scroll Registers (SCX/SCY)
- Pan Docs: Memory Map, VRAM (0x8000-0x9FFF)
Educational Integrity
What I Understand Now
- BG Tilemap Base Select: LCDC bit3 controls which tilemap is used (0x9800 vs 0x9C00). It is critical to use the correct tilemap according to the bit.
- Scroll (SCX/SCY): Applied with wrap 0..255. The calculation of map_x and map_y must include scrolling correctly.
- VRAM reading: Must be used
read_vram()to read from VRAM (0x8000-0x9FFF), noread()direct, to respect access restrictions during PPU mode.
What remains to be confirmed
- Visual Validation: Verify with grid UI that the fix improves the rendering of problematic ROMs (Pokémon, Tetris).
- Automatic Diagnosis: If LCDC bit3=1 and tilemap_nz_9C00 >> tilemap_nz_9800 but the renderer is using 0x9800 → BUG confirmed.
Hypotheses and Assumptions
If the problem persists after the fix, it may be in Window enable (LCDC bit5), VRAM bank/attrs CGB, or in other components (OBJ, palettes).
Next Steps
- [ ] Real validation with grid UI to verify that the fix improves rendering
- [ ] Log analysis [PPU-TILEMAP-DIAG] to identify which tilemap has data in problematic ROMs
- [ ] If the problem persists, investigate Window enable (LCDC bit5) and VRAM bank/attrs CGB