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

Tile Loading Investigation and Correction

Date:2025-12-25 StepID:0291 State: VERIFIED

Summary

Implemented a full set of diagnostic monitors to investigate why tiles are not loading into VRAM. Analysis of Step 0290 confirmed that [TILE-LOAD] detects 0 tile loads, which means the game is not writing tile data to VRAM. Five new monitors were implemented: [VRAM-INIT] to check the initial state of VRAM, [TILE-LOAD-EXTENDED] to capture ALL writes with timing context, [CLEANUP-TRACE] to track the VRAM cleanup routine (PC:0x36E3), [BLOCK-WRITE] to detect consecutive tile loads, and a PPU frame counter to track the timing of operations.

Hardware Concept

Loading Tiles in VRAM:Tiles are typically loaded during game initialization. They can be loaded from ROM (direct or compressed), from RAM (pre-processed data), or using DMA (although usually DMA is for OAM). Tiles can be loaded before the first frame or during V-Blank. If monitors are activated only after starting to render, early loads may be lost.

Load Timing:Loading can occur during initialization (before the first frame), during V-Blank (when VRAM is accessible), or during H-Blank (in some cases). If monitors are activated only after starting to render, early loads may be lost.

VRAM Initial State:In real hardware, VRAM is initialized to random values ​​at power-up. Games usually clear VRAM before loading their own data. Some games expect VRAM to have specific values ​​(rare). The [VRAM-INIT] monitor checks the initial state of VRAM after loading the ROM to understand if the game expects VRAM to have data from startup or if loading is the game's responsibility.

Cleaning Routines:Many games run cleanup routines that write zeros to VRAM before loading their own data. Step 0288 identified that PC:0x36E3 is writing zeros to VRAM. The [CLEANUP-TRACE] monitor tracks this routine to understand what exactly it does and if there is code afterward that should load tiles.

Fountain:Pan Docs - "Video RAM (VRAM)", "Tile Data", "DMA Transfer", "LCD Timing"

Implementation

1. Frame Counter in PPU

Added a global frame counter in PPU that increments every time LY returns to 0 (new frame). This counter is necessary to track the tile loading timing and determine whether tiles are loaded before frame 0 or during initialization.

// In PPU.hpp
uint64_t frame_counter_;

// In PPU.cpp constructor
frame_counter_(0)

// In PPU.cpp step(), when LY > 153
if (ly_ > 153) {
    ly_ = 0;
    frame_counter++;  // Increase frame counter
    //...
}

// Public method to get the current frame
uint64_t PPU::get_frame_counter() const {
    return frame_counter_;
}

2. Monitor [VRAM-INIT] - Initial Status Inspection

Function implementedMMU::inspect_vram_initial_state()which is called fromMMU::load_rom()after loading the ROM. This function checks the initial state of VRAM (0x8000-0x97FF) and reports how many non-zero bytes there are, the first address with non-zero data, and the checksum of the initial tilemap (0x9800).

void MMU::inspect_vram_initial_state() {
    // Check if VRAM has non-zero data
    int non_zero_count = 0;
    uint16_t first_non_zero_addr = 0xFFFF;
    for (uint16_t addr = 0x8000; addr<= 0x97FF; addr++) {
        if (memory_[addr] != 0x00 && memory_[addr] != 0x7F) {
            non_zero_count++;
            if (first_non_zero_addr == 0xFFFF) {
                first_non_zero_addr = addr;
            }
        }
    }
    
    printf("[VRAM-INIT] Estado inicial de VRAM: %d bytes no-cero (0x8000-0x97FF)\n", non_zero_count);
    if (first_non_zero_addr != 0xFFFF) {
        printf("[VRAM-INIT] Primer byte no-cero en: 0x%04X (valor: 0x%02X)\n",
               first_non_zero_addr, memory_[first_non_zero_addr]);
    } else {
        printf("[VRAM-INIT] VRAM está completamente vacía (solo ceros)\n");
    }
    
    // Verificar checksum del tilemap inicial
    uint16_t tilemap_checksum = 0;
    for (int i = 0; i < 1024; i++) {
        tilemap_checksum += memory_[0x9800 + i];
    }
    printf("[VRAM-INIT] Checksum del tilemap (0x9800): 0x%04X\n", tilemap_checksum);
}

3. Monitor [TILE-LOAD-EXTENDED] - Extended Capture

Extended the [TILE-LOAD] monitor to capture ALL writes to Tile Data (0x8000-0x97FF), including cleanup (0x00) but marking them differently. The monitor now tracks the current frame using the PPU frame counter and marks whether the write occurs during initialization (first 100 writes) or after.

// --- Step 0291: Extended Tiles Load Monitor ([TILE-LOAD-EXTENDED]) ---
if (addr >= 0x8000 && addr<= 0x97FF) {
    // Obtener contador de frames desde PPU si está disponible
    uint64_t frame_counter = 0;
    if (ppu_ != nullptr) {
        frame_counter = ppu_->get_frame_counter();
    }
    
    // Mark the end of initialization (approximately after 100 writes)
    static int write_count = 0;
    static bool is_initialization = true;
    write_count++;
    if (write_count > 100) {
        is_initialization = false;
    }
    
    // Capture ALL writes, marking whether they are cleanup or real data
    static int tile_load_extended_count = 0;
    if (tile_load_extended_count< 1000) {
        bool is_data = (value != 0x00 && value != 0x7F);
        uint16_t tile_offset = addr - 0x8000;
        uint8_t tile_id_approx = tile_offset / 16;
        
        printf("[TILE-LOAD-EXT] %s | Write %04X=%02X (TileID~%d) PC:%04X Frame:%llu Init:%s\n",
               is_data ? "DATA" : "CLEAR",
               addr, value, tile_id_approx, debug_current_pc, frame_counter,
               is_initialization ? "YES" : "NO");
        tile_load_extended_count++;
    }
}

4. Monitor [CLEANUP-TRACE] - Cleaning Routine Trace

A monitor was implemented that tracks the execution around PC:0x36E3 to understand what this routine does and if there is code after it loads tiles. Step 0288 identified that PC:0x36E3 is writing zeros to VRAM. The monitor captures the opcode at each address around 0x36E3.

// --- Step 0291: VRAM Cleaning Routine Trace (PC:0x36E3) ---
if (debug_current_pc >= 0x36E0 && debug_current_pc<= 0x36F0) {
    static int cleanup_trace_count = 0;
    if (cleanup_trace_count < 200) {
        uint8_t op = read(debug_current_pc);
        printf("[CLEANUP-TRACE] PC:0x%04X OP:0x%02X | Bank:%d\n",
               debug_current_pc, op, current_rom_bank_);
        cleanup_trace_count++;
    }
}

5. [BLOCK-WRITE] Monitor - Consecutive Write Detection

Implemented a monitor that detects consecutive writes to VRAM that could be loading tiles in bulk (like a copy loop). The monitor detects when 16 consecutive bytes are written (a complete tile) and reports the address range and the PC that originated the writing.

// --- Step 0291: Block Write Monitor ([BLOCK-WRITE]) ---
if (addr >= 0x8000 && addr<= 0x97FF) {
    static uint16_t last_vram_addr = 0xFFFF;
    static int consecutive_writes = 0;
    if (addr == last_vram_addr + 1) {
        consecutive_writes++;
        if (consecutive_writes == 16) {  // Un tile completo
            printf("[BLOCK-WRITE] Posible carga de tile en bloque: 0x%04X-0x%04X desde PC:0x%04X\n",
                   addr - 15, addr, debug_current_pc);
        }
    } else {
        consecutive_writes = 0;
    }
    last_vram_addr = addr;
}

Hypotheses to investigate

The implemented monitors allow four main hypotheses to be investigated:

  1. Hypothesis 1: Timing- Are the tiles loaded before frame 0 (during initialization before the monitors are activated)? The [TILE-LOAD-EXTENDED] monitor with frame counter allows you to verify this.
  2. Hypothesis 2: Erasure- Are tiles loaded but then deleted immediately afterwards? The [CLEANUP-TRACE] monitor traces the cleaning routine to verify this.
  3. Hypothesis 3: Alternative methods- Does the game use DMA or compression to load tiles that we don't detect? The [BLOCK-WRITE] monitor detects block uploads.
  4. Hypothesis 4: Initial state- Should VRAM have data from the start (from the builder)? The [VRAM-INIT] monitor checks the initial state.

Affected Files

  • src/core/cpp/PPU.hpp- Added frame counter and getter method
  • src/core/cpp/PPU.cpp- Implementation of the frame counter
  • src/core/cpp/MMU.hpp- Added inspect_vram_initial_state() function
  • src/core/cpp/MMU.cpp- Implementation of all diagnostic monitors

Tests and Verification

The implementation was validated by successful compilation of the C++/Cython module:

python setup.py build_ext --inplace

Result:Successful compilation without errors (only minor non-critical warnings).

Compiled C++ module validation:The module has been compiled successfully and is ready to run with the new monitors active. The monitors will be activated automatically when the emulator is run and will generate logs that will allow the proposed hypotheses to be analyzed.

Next verification steps:Run the emulator with a test ROM and analyze the logs generated by the monitors to determine which of the hypotheses is correct (or if it is a combination of them).

Sources consulted

Educational Integrity

What I Understand Now

  • Tiles Loading Timing:Tiles can be loaded at different times: during initialization (before the first frame), during V-Blank, or during H-Blank. If monitors are activated only after starting to render, early loads may be lost.
  • VRAM Initial State:In real hardware, VRAM is initialized to random values ​​at power-up. Games usually clear VRAM before loading their own data. Some games expect VRAM to have specific values ​​(rare).
  • Cleaning Routines:Many games run cleanup routines that write zeros to VRAM before loading their own data. These routines can be critical to understanding the tile loading flow.
  • Alternative Loading Methods:Games can use alternative methods to load tiles, such as DMA (although DMA is typically for OAM), compression/decompression, or block writes via loops.

What remains to be confirmed

  • Log Analysis:We need to run the emulator with the new monitors active and analyze the generated logs to determine which of the hypotheses is correct (or if it is a combination of them).
  • Corrections Based on Findings:Once we identify the root cause of the problem, we will need to apply the corresponding fixes (modify monitor timing, prevent premature deletion, load initial tiles, implement support for alternative methods, etc.).

Hypotheses and Assumptions

We assume that the problem is related to tile loading timing or alternative loading methods that we are not detecting. The monitors implemented are designed to capture all possible scenarios and allow a thorough analysis of the problem.

Next Steps

  • [ ] Run the emulator with the new monitors active
  • [ ] Analyze the logs generated by the monitors
  • [ ] Determine which of the hypotheses is correct (or if it is a combination)
  • [ ] Apply appropriate corrections based on findings
  • [ ] Verify that tiles load correctly after corrections
  • [ ] Confirm that the screen displays graphics correctly