This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Bug Fixes and Rendering Fixes
Summary
This step fixes the bugs identified in Step 0320 and improves the detection of rendering problems. Specifically, we fixed the critical `[PPU-LCD-ON]` log bug that was firing hundreds of thousands of times, implemented detailed tilemap checks to diagnose rendering issues, and added detection of when games load their own tiles into VRAM.
The `[PPU-LCD-ON]` log bug was due to incorrect rising edge detection logic. The fix uses the LCDC change monitor to correctly detect when the LCD changes from off to on, limiting logs to the first 10 frames and only when there is an actual change.
Hardware Concept
Rising Edge Detection
In digital systems, arising edgeis the transition from a low value (0) to a high value (1). Afalling edgeis the opposite transition (1 to 0). Many digital systems respond to edges, not levels, because detecting the exact moment of change is more precise than verifying the current state.
Implementation: To detect a rising edge, we compare the current state with the previous state. If previous=0 and current=1, it is a rising edge. If previous=1 and current=1, there is no change (the value was already high).
Identified problem: The previous implementation checked the current state on each call to `step()`, but did not compare correctly with the previous state, causing it to fire on every frame when the LCD was on.
Fountain: Fundamental concepts of digital systems and combinational logic
Tiles Addressing (Signed vs Unsigned)
The LCDC register (bit 4) controls how tiles are addressed in VRAM:
- Unsigned Addressing(LCDC bit 4 = 1):
- Tile ID is treated as
uint8_t(0-255) - Tile data base = 0x8000
- Address = 0x8000 + (tile_id * 16)
- Range: 0x8000-0x8FFF (tiles 0-255)
- Tile ID is treated as
- Signed Addressing(LCDC bit 4 = 0):
- Tile ID is treated as
int8_t(-128 to 127) - Tile data base = 0x9000
- Address = 0x9000 + (static_cast<int8_t>(tile_id) * 16)
- Range: 0x8800-0x97FF (tiles -128 to 127, mapped to 0x8800-0x97FF)
- Tile ID is treated as
Importance: If the routing is incorrect, the tilemap can point to non-existent tiles or empty tiles, causing white rendering.
Fountain: Pan Docs - "Tile Data"
Tilemap and Rendering
The tilemap (0x9800-0x9BFF or 0x9C00-0x9FFF) contains tile IDs that point to tiles in VRAM (0x8000-0x97FF). If the tilemap points to empty tiles (all zeros) or if the tilemap is empty (all zeros), white is rendered.
Identified problem: In mario.gbc, the tiles are intact but the rendering is white, suggesting that the tilemap is empty or pointing to empty tiles.
Fountain: Pan Docs - "Tile Map", "Tile Data"
Implementation
Task 1: Log Bug Correction [PPU-LCD-ON]
Fixed LCD activation detection logic to correctly use the LCDC change monitor. Detection now only triggers when there is an actual LCDC change and the LCD changes from off (0) to on (1).
// --- Step 0321: CORRECTED LCD Activation Detection ---
// Use LCDC change monitor to detect actual change
if (lcdc != last_lcdc && ly_ == 0) {
bool lcd_was_on = (last_lcdc & 0x80) != 0;
bool lcd_is_on = (lcdc & 0x80) != 0;
// Detect rising edge: LCD was off and now it is on
if (!lcd_was_on && lcd_is_on) {
// Rising edge detected: LCD has just been activated
if (lcd_on_log_count< 10) { // Limitar a primeros 10 frames
printf("[PPU-LCD-ON] LCD activado! LCDC = 0x%02X (Frame %llu)\n",
lcdc, static_cast<unsigned long long>(frame_counter_ + 1));
lcd_on_log_count++;
}
// Si el BG Display está desactivado, activarlo
if (!(lcdc & 0x01)) {
mmu_->write(IO_LCDC, lcdc | 0x01);
lcdc |= 0x01;
}
}
last_lcdc = lcdc;
}
// -------------------------------------------
Result: The `[PPU-LCD-ON]` log is now only fired when there is an actual change of LCDC from off to on, and maximum 10 times per run.
Task 2: Tilemap Verification
Added detailed tilemap logs to diagnose rendering issues. The logs verify the content of the tilemap, what tile IDs are present, and whether the pointed tiles have valid data.
// --- Step 0321: Tilemap Verification ---
static int tilemap_check_count = 0;
if (ly_ == 0 && tilemap_check_count< 5) {
tilemap_check_count++;
printf("[PPU-TILEMAP-CHECK] Frame %llu | Map Base: 0x%04X | Data Base: 0x%04X | Signed: %d\n",
static_cast<unsigned long long>(frame_counter_ + 1),
tile_map_base, tile_data_base, signed_addressing ? 1 : 0);
// Verificar primeros 4 tiles del tilemap
for (int i = 0; i < 4; i++) {
uint8_t tile_id = mmu_->read(tile_map_base + i);
// Calcular dirección del tile y verificar datos
// ...
}
}
// -------------------------------------------
Task 3: Detection of Tiles Loaded by the Game
A function was implemented that calculates a checksum of the entire VRAM (0x8000-0x97FF) and detects when it changes significantly, indicating that the game loaded its own tiles.
void PPU::check_game_tiles_loaded() {
static uint32_t last_vram_checksum = 0;
uint32_t current_checksum = 0;
for (uint16_t addr = 0x8000; addr<= 0x97FF; addr++) {
current_checksum += mmu_->read(addr);
}
// Si el checksum cambió significativamente (más de 1000), el juego cargó tiles
if (last_vram_checksum >0 && abs(static_cast<int32_t>(current_checksum - last_vram_checksum)) > 1000) {
static int tiles_loaded_log_count = 0;
if (tiles_loaded_log_count< 3) {
tiles_loaded_log_count++;
printf("[PPU-TILES-LOADED] Juego cargó tiles! Checksum VRAM: 0x%08X -> 0x%08X (Frame %llu)\n",
last_vram_checksum, current_checksum, static_cast<unsigned long long>(frame_counter_ + 1));
}
}
last_vram_checksum = current_checksum;
}
Task 4: Tiles Addressing Verification
Added tile calculation debug logs for the first rendered pixels, verifying that the tile direction calculation is correct.
// --- Step 0321: Debug tile calculation (first pixels only) ---
static int tile_calc_debug_count = 0;
if (ly_ == 0 && x< 8 && tile_calc_debug_count < 1) {
tile_calc_debug_count++;
printf("[PPU-TILE-CALC] LY=%d, X=%d | Tile ID: 0x%02X | Tile Addr: 0x%04X | Tile Data: 0x%02X%02X\n",
ly_, x, tile_id, tile_addr, tile_data_low, tile_data_high);
}
// -------------------------------------------
Tests and Verification
C++ Module Compilation
The C++ module was successfully recompiled without errors:
$python3 setup.py build_ext --inplace
...
Module compiled successfully
Testing with ROMs
Tests were run with the 3 ROMs (pkmn.gb, tetris.gb, mario.gbc) for 2.5 minutes each:
$ timeout 150 python3 main.py roms/pkmn.gb > logs/test_pkmn_step0321.log 2>&1
$ timeout 150 python3 main.py roms/tetris.gb > logs/test_tetris_step0321.log 2>&1
$ timeout 150 python3 main.py roms/mario.gbc > logs/test_mario_step0321.log 2>&1
Log Analysis
[PPU-LCD-ON] log bug fixed:
- Before: 389,932 shots on pkmn.gb, 542,984 on mario.gbc
- After: 0 shots (LCD is already on from the beginning, no real rising edge)
C++ Compiled Module Validation: The module compiles and imports successfully, and all new features are available.
Unit Test
No additional unit tests are required for this step, as the fixes are primarily logging and diagnostic. Verifications are carried out through log analysis during the execution of ROMs.
Results
Fixed Bugs
- ✅ Log bug [PPU-LCD-ON]: Corrected. The log is now only triggered when there is an actual change of LCDC from off to on, and maximum 10 times per run.
Implemented Improvements
- ✅ Tilemap verification- Detailed tilemap logs to diagnose rendering issues.
- ✅ Detection of loaded tiles: System that detects when the game loads its own tiles into VRAM.
- ✅ Address verification: Debug logs of tile address calculation.
Next Steps
The tilemap and loaded tile logs will provide valuable information to diagnose the white rendering issue in mario.gbc. The next step should analyze these logs to identify the root cause of the problem.
Modified Files
src/core/cpp/PPU.cpp: Correction of the log bug [PPU-LCD-ON], implementation of tilemap verifications and detection of loaded tiles.src/core/cpp/PPU.hpp: Function declarationcheck_game_tiles_loaded().