This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Threshold Correction and Tilemap Analysis
Summary
This step corrects the detection threshold for real tiles (reducing it from 500 to 200 bytes) which was too high and prevented valid tiles from being detected. Verifications were implemented that run independently of the initial state of VRAM, allowing diagnosis even before detecting real tiles.
Added correspondence analysis between loaded tiles and tilemap to identify which tile IDs should point to the actual tiles, and tilemap update sequence analysis to detect if the tilemap is updated after loading tiles. The logs reveal that the tilemap has non-zero tile IDs but VRAM is empty, confirming the problem identified in Step 0325.
Hardware Concept
Detection Thresholds
Detection thresholds must balance false positives and false negatives:
- Threshold too high: Does not detect valid cases (false negatives). In our case, 500 bytes was too high to detect 20 tiles (320 bytes).
- Threshold too low: Detects invalid cases (false positives), such as noise or residual data.
- Optimal threshold: It must be between the expected minimum and maximum noise. For tiles, 20 tiles = 320 bytes, a threshold of 200 bytes (12 tiles) is reasonable.
Fountain: Empirical analysis based on observed behavior of the games.
Tilemap-Tiles correspondence
The tilemap contains tile IDs that point to tiles in VRAM. If the tiles are loaded at specific addresses (ex: 0x8820+) but the tilemap points to other addresses (ex: 0x9000+), there is no correspondence. Games should update the tilemap to point to the actual loaded tiles. If the tilemap is not updated, blank or test pattern is rendered.
Fountain: Pan Docs - "Tile Map", "Tile Data"
Initialization Sequence
Typical initialization pattern:
- Turn off LCD
- Clear VRAM (write zeros)
- Load tiles into VRAM
- Update tilemap to point to loaded tiles
- Activate LCD
If the tilemap is not updated after loading tiles, the loaded tiles are not rendered. Some games may update the tilemap later in the run.
Fountain: Pan Docs - "Power Up Sequence", empirical observation
Implementation
1. Correction of the Detection Threshold
Threshold reduced from 500 bytes to 200 bytes (approx. 12 full tiles). This allows detecting when there are 20+ tiles loaded (320 bytes). Added diagnostic logs that show the number of non-zero bytes each time it is checked, making it easier to adjust the threshold if necessary.
Modified file: src/core/cpp/PPU.cpp(lines 511-535)
// Fixed threshold: 200 bytes instead of 500
bool has_tiles_now = (non_zero_bytes > 200);
// Diagnostic logs
static int vram_diag_count = 0;
if (vram_diag_count< 10) {
vram_diag_count++;
printf("[PPU-VRAM-DIAG] Frame %llu | Non-zero bytes: %d/6144 | Umbral: 200 | Detectado: %d\n",
frame_counter_ + 1, non_zero_bytes, has_tiles_now ? 1 : 0);
}
2. Independent Verifications of the Initial State
Tilemap checks now always run, regardless of whethervram_has_tilesis true or false. This allows the problem to be diagnosed even before real tools are detected. The checks run every 60 frames and show the current state of VRAM.
Modified file: src/core/cpp/PPU.cpp(lines 647-669)
// Checks always on (does not depend on vram_has_tiles)
static int tilemap_verify_count = 0;
if (ly_ == 0 && tilemap_verify_count< 5 && (frame_counter_ % 60 == 0)) {
tilemap_verify_count++;
// Verificar primeros 32 bytes del tilemap
int valid_tile_ids = 0;
for (int i = 0; i < 32; i++) {
uint8_t tile_id = mmu_->read(tile_map_base + i);
if (tile_id != 0x00) {
valid_tile_ids++;
}
}
printf("[PPU-TILEMAP-VERIFY] Frame %llu | Tilemap has %d/32 non-zero tile IDs | VRAM has tiles: %d\n",
frame_counter_ + 1, valid_tile_ids, vram_has_tiles ? 1 : 0);
}
3. Correspondence Analysis of Loaded Tiles - Tilemap
Added analysis that checks for tiles at the addresses where they were loaded (0x8820+) and calculates which tile IDs should point to those addresses based on addressing (signed/unsigned). This allows you to identify if the tilemap points to the actual loaded tiles.
Modified file: src/core/cpp/PPU.cpp(lines 577-600)
// Correspondence analysis every 2 seconds
if (ly_ == 0 && (frame_counter_ % 120 == 0)) {
int tiles_found_in_vram = 0;
for (uint16_t check_addr = 0x8820; check_addr<= 0x8A80; check_addr += 0x20) {
uint8_t tile_byte1 = mmu_->read(check_addr);
uint8_t tile_byte2 = mmu_->read(check_addr + 1);
if (tile_byte1 != 0x00 || tile_byte2 != 0x00) {
tiles_found_in_vram++;
// Calculate which tile ID should point to this address
if (signed_addressing) {
int16_t offset = check_addr - 0x9000;
int8_t tile_id = static_cast(offset / 16);
printf("[PPU-TILES-MAP] Tile at 0x%04X corresponds to TileID: 0x%02X\n",
check_addr, static_cast(tile_id));
}
}
}
}
4. Tilemap Update Sequence Analysis
Added analysis in MMU that detects when tiles are loaded (writing valid data to VRAM) and then checks if the tilemap is updated afterwards. This allows the temporal sequence to be identified: tile loading → tilemap update.
Modified file: src/core/cpp/MMU.cpp(lines 751-775)
// Detect when tiles are loaded
static bool tiles_were_loaded = false;
if (addr >= 0x8000 && addr<= 0x97FF && value != 0x00) {
// Verificar si el tile completo tiene datos
uint16_t tile_base = (addr / 16) * 16;
bool tile_has_data = false;
for (int i = 0; i < 16; i++) {
if (tile_base + i < 0x9800 && memory_[tile_base + i] != 0x00) {
tile_has_data = true;
break;
}
}
if (tile_has_data) {
tiles_were_loaded = true;
}
}
// Si el tilemap se actualiza después de cargar tiles
if ((addr >= 0x9800 && addr<= 0x9BFF) || (addr >= 0x9C00 && addr<= 0x9FFF)) {
static int tilemap_update_after_tiles = 0;
if (tiles_were_loaded && value != 0x00 && tilemap_update_after_tiles < 10) {
tilemap_update_after_tiles++;
printf("[TILEMAP-SEQ] Tilemap actualizado DESPUÉS de cargar tiles! PC:0x%04X | Addr:0x%04X | TileID:0x%02X\n",
debug_current_pc, addr, value);
}
}
Affected Files
src/core/cpp/PPU.cpp- Threshold correction, independent verifications, correspondence analysissrc/core/cpp/MMU.cpp- Tilemap update sequence analysis
Tests and Verification
Tests were run with the 3 test ROMs (pkmn.gb, tetris.gb, mario.gbc) for 2.5 minutes each to verify the implemented corrections.
Commands Executed
timeout 150 python3 main.py roms/pkmn.gb > logs/test_pkmn_step0326.log 2>&1
timeout 150 python3 main.py roms/tetris.gb > logs/test_tetris_step0326.log 2>&1
timeout 150 python3 main.py roms/mario.gbc > logs/test_mario_step0326.log 2>&1
Log Analysis (pkmn.gb)
The logs show:
- Threshold working: 40 non-zero bytes are detected in frame 1, but it does not reach the 200 byte threshold (correct).
- Independent verifications: Checks run successfully even when VRAM is empty.
- Confirmed problem: The tilemap has non-zero tile IDs (15/32 in frame 1, 32/32 in later frames) but VRAM is empty (0 non-zero bytes). This confirms that the tilemap does not point to real tiles.
- No real tools detected: The 200 byte threshold was never reached during execution, indicating that the tiles are not loaded or cleaned up immediately afterwards.
Log Example
[PPU-VRAM-DIAG] Frame 1 | Non-zero bytes: 40/6144 | Threshold: 200 | Detected: 0
[PPU-TILEMAP-VERIFY] Frame 1 | Tilemap has 15/32 non-zero tile IDs | VRAM has tiles: 0
[PPU-VRAM-DIAG] Frame 61 | Non-zero bytes: 0/6144 | Threshold: 200 | Detected: 0
[PPU-TILEMAP-VERIFY] Frame 61 | Tilemap has 32/32 non-zero tile IDs | VRAM has tiles: 0
C++ Compiled Module Validation: The module was successfully recompiled without errors (only minor formatting warnings).
Sources consulted
- Pan Docs: "Tile Map" - Tilemap structure and correspondence with tiles
- Pan Docs: "Tile Data" - Signed/unsigned addressing of tiles
- Pan Docs: "Power Up Sequence" - Typical initialization sequence
Educational Integrity
What I Understand Now
- Detection thresholds: They must balance false positives and false negatives. A threshold that is too high prevents valid cases from being detected.
- tilemap-tiles correspondence: The tilemap can have non-zero tile IDs but point to empty tiles if the tiles are not loaded or cleared afterwards.
- Initialization sequence: Games may clear VRAM after loading tiles, or load tiles but not update the tilemap immediately.
What remains to be confirmed
- Why is the tilemap not updating?: The logs show that the tilemap has non-zero tile IDs but VRAM is empty. We need to investigate if the tiles are loaded and then cleaned, or if they are never loaded.
- Tiles loading timing: When exactly are the tiles loaded? Before or after activating the LCD?
- Tilemap update: Is the tilemap updated after loading tiles, or is it never updated?
Hypotheses and Assumptions
Main hypothesis: Games either clear VRAM after loading tiles (as observed in Step 0323 with PC:0x36E3), or load tiles but do not update the tilemap to point to them. The tilemap may contain tile IDs from a previous initialization that are no longer valid.
Next Steps
- [ ] Analyze complete logs of the 3 ROMs to identify common patterns
- [ ] Investigate if the tiles are loaded and then cleaned, or if they are never loaded
- [ ] Check if the tilemap is updated after loading tiles using the [TILEMAP-SEQ] logs
- [ ] If the cause is identified, implement a solution so that the rendering uses the real tiles when the tilemap references them