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

Investigation of Early Writes and VRAM Access Restrictions

Date:2025-12-29 StepID:0353 State: VERIFIED

Summary

Implemented VRAM monitoring from the start of run to investigate why games write only zeros (0x00) to VRAM during run. Verified initial state of VRAM when loading ROM, investigated VRAM access restrictions when LCD=ON, and monitored LCD state changes. The results show that games DO have initial data in VRAM (92-98% non-zero bytes), but all writes during execution are zeros (0% non-zero writes). The LCD turns off and on during execution, but when it turns off, VRAM already has only 40 non-zero bytes (0.65%), suggesting that the initial data is erased before the game can load new tiles.

Hardware Concept

VRAM Access Restrictions:On actual Game Boy hardware, access to VRAM is restricted when the LCD is on. VRAM can only be accessed during VBLANK (when the LCD is in VBLANK mode, LY >= 144) or when the LCD is completely off (LCDC bit 7 = 0). Writing to VRAM when the LCD is on (out of VBLANK) can cause problems or be ignored by the hardware. This restriction is critical because the PPU is constantly reading VRAM during rendering, and writing while reading can cause data corruption.

LCD Timing:Games often turn off the LCD to safely load tiles into VRAM. After loading the tiles, the games turn on the LCD. The LCD can be turned off/on using the LCDC register (bit 7). When the LCD is off, the PPU stops and LY stays at 0, allowing the game to modify VRAM without restrictions. If a game attempts to write tiles when the LCD is on, the writes may be ignored or cause corruption.

Tiles Loading:Tiles are loaded into VRAM during game initialization or during execution when the LCD is off. Tiles are written in sequences of 16 consecutive bytes (each tile is 8x8 pixels, 2 bytes per line, 8 lines = 16 bytes). If a game writes only 0x00 to VRAM, it is not loading actual tiles, it is just clearing VRAM. If a game has initial data in VRAM (from ROM or from initialization), but then writes only zeros, the initial data is being erased.

VRAM Initial State:Some games may have initial data in VRAM from ROM or from game initialization. This data can be test tiles, Nintendo logo tiles, or basic game tiles. If this data is deleted (by writing 0x00) before the game loads new tiles, VRAM will be empty and nothing will be displayed on the screen.

Implementation

4 main diagnostic tasks were implemented according to the Step 0353 plan:

1. VRAM Monitoring From Start of Execution (MMU.cpp)

Added code inMMU::write()to monitor VRAM from the start of execution, not just after activating logs. The code initializes monitoring on the first write to VRAM and logs all non-zero writes (up to 200) with information about the status of the LCD and PC.

// --- Step 0353: Monitoring from Start ---
static int vram_write_from_start_count = 0;
static int vram_write_non_zero_from_start = 0;
static bool vram_monitoring_initialized = false;

if (!vram_monitoring_initialized) {
    vram_monitoring_initialized = true;
    printf("[MMU-VRAM-MONITOR-INIT] VRAM monitoring initialized from startup\n");
}

if (addr >= 0x8000 && addr< 0x9800) {
    vram_write_from_start_count++;
    
    if (value != 0x00) {
        vram_write_non_zero_from_start++;
        
        // Loggear todas las escrituras no-cero (hasta 200)
        if (vram_write_non_zero_from_start <= 200) {
            uint16_t pc = debug_current_pc;
            bool lcd_is_on = false;
            if (ppu_ != nullptr) {
                lcd_is_on = ppu_->is_lcd_on();
            }
            
            printf("[MMU-VRAM-WRITE-FROM-START] Non-zero write #%d | Addr=0x%04X | Value=0x%02X | "
                   "PC=0x%04X | LCD=%s | Total writes=%d\n",
                   vram_write_non_zero_from_start, addr, value, pc,
                   lcd_is_on ? "ON" : "OFF", vram_write_from_start_count);
        }
    }
    
    //Log statistics every 1000 writes
    if (vram_write_from_start_count % 1000 == 0) {
        printf("[MMU-VRAM-WRITE-FROM-START-STATS] Total writes=%d | Non-zero writes=%d | "
               "Non-zero ratio=%.2f%%\n",
               vram_write_from_start_count, vram_write_non_zero_from_start,
               (vram_write_non_zero_from_start * 100.0) / vram_write_from_start_count);
    }
}
// -------------------------------------------

2. Verification of the Initial State of VRAM (MMU.cpp)

Feature addedMMU::check_initial_vram_state()which is called fromMMU::load_rom()to check the initial state of VRAM when loading the ROM. The function counts non-zero bytes and full tiles in VRAM, and generates a log with statistics.

// --- Step 0353: VRAM Initial State Verification ---
void MMU::check_initial_vram_state() {
    int non_zero_bytes = 0;
    int complete_tiles = 0;
    
    for (uint16_t addr = 0x8000; addr< 0x9800; addr += 16) {
        int tile_non_zero = 0;
        
        for (int i = 0; i < 16; i++) {
            uint8_t byte = memory_[addr - 0x8000 + i];
            if (byte != 0x00) {
                non_zero_bytes++;
                tile_non_zero++;
            }
        }
        
        if (tile_non_zero >= 8) {
            complete_tiles++;
        }
    }
    
    printf("[MMU-VRAM-INITIAL-STATE] VRAM initial state | Non-zero bytes: %d/6144 (%.2f%%) | "
           "Complete tiles: %d/384 (%.2f%%)\n",
           non_zero_bytes, (non_zero_bytes * 100.0) / 6144,
           complete_tiles, (complete_tiles * 100.0) / 384);
    
    if (non_zero_bytes > 200) {
        printf("[MMU-VRAM-INITIAL-STATE] ✅ VRAM has initial data (possibly from ROM)\n");
    } else {
        printf("[MMU-VRAM-INITIAL-STATE] ⚠️ VRAM is empty at startup\n");
    }
}
// -------------------------------------------

3. Verification of VRAM Access Restrictions When LCD=ON (MMU.cpp)

Added code inMMU::write()to check for VRAM access restrictions when LCD=ON. The code detects non-zero writes to VRAM when the LCD is on and we are not in VBLANK, and logs these writes (up to 50) with information about the state of the LCD.

// --- Step 0353: Verification of VRAM Access Restrictions ---
bool lcd_is_on = false;
bool in_vblank = false;

if (ppu_ != nullptr) {
    lcd_is_on = ppu_->is_lcd_on();
    uint8_t ly = ppu_->get_ly();
    in_vblank = (ly >= 144);
}

bool access_restricted = lcd_is_on && !in_vblank;

if (access_restricted && value != 0x00) {
    static int restricted_access_count = 0;
    restricted_access_count++;
    
    if (restricted_access_count<= 50) {
        uint16_t pc = debug_current_pc;
        
        printf("[MMU-VRAM-ACCESS-RESTRICTED] Write #%d | Addr=0x%04X | Value=0x%02X | "
               "PC=0x%04X | LCD=ON, not in VBLANK | Access should be restricted\n",
               restricted_access_count, addr, value, pc);
    }
}
// TODO: Implementar restricciones de acceso a VRAM cuando LCD=ON (excepto VBLANK)
// -------------------------------------------

4. Verification of LCD State Changes (PPU.cpp)

Added code inPPU::step()to verify LCD state changes during execution. The code detects when the LCD turns off or on, and when it turns off, it checks the state of VRAM (how many non-zero bytes it has).

// --- Step 0353: Verification of LCD State Changes ---
static bool last_lcd_state_step0353 = false;
static int lcd_state_change_count = 0;

bool current_lcd_state = lcd_enabled;

if (current_lcd_state != last_lcd_state_step0353) {
    lcd_state_change_count++;
    
    printf("[PPU-LCD-STATE-CHANGE] Change #%d | Frame %llu | LY: %d | "
           "LCD changed from %s to %s\n",
           lcd_state_change_count,
           static_cast(frame_counter_),
           ly_,
           last_lcd_state_step0353 ? "ON" : "OFF",
           current_lcd_state ? "ON" : "OFF");
    
    // If LCD turns off, check VRAM status
    if (!current_lcd_state) {
        int non_zero_bytes = 0;
        for (uint16_t addr = 0x8000; addr< 0x9800; addr++) {
            uint8_t byte = mmu_->read(addr);
            if (byte != 0x00) {
                non_zero_bytes++;
            }
        }
        
        printf("[PPU-LCD-STATE-CHANGE] LCD OFF | VRAM non-zero bytes: %d/6144 (%.2f%%)\n",
               non_zero_bytes, (non_zero_bytes * 100.0) / 6144);
    }
    
    last_lcd_state_step0353 = current_lcd_state;
}
// -------------------------------------------

Affected Files

  • src/core/cpp/MMU.cpp- Added startup monitoring, VRAM initial status check, and access restrictions check
  • src/core/cpp/MMU.hpp- Added declarationcheck_initial_vram_state()
  • src/core/cpp/PPU.cpp- Added verification of LCD state changes

Tests and Verification

Tests were run with the 5 ROMs in parallel (~2.5 minutes total) to analyze the behavior of VRAM:

  • Tested ROMs:pkmn.gb, tetris.gb, mario.gbc, pkmn-amarillo.gb, Oro.gbc
  • Command executed: timeout 150 python3 main.py roms/<rom>.gb 2>&1 | tee logs/test_<rom>_step0353.log
  • Log analysis:commands were usedgrepandheadto extract specific information without cluttering the context

Key Results

  • Monitoring from the beginning:✅ Initialized correctly on all ROMs
  • Non-zero writes:❌ 0 non-zero writes on all ROMs (0% non-zero writes)
  • VRAM initial state:✅ Games DO have initial data (92-98% non-zero bytes)
  • Access restrictions:⚠️ No restricted access detected (no non-zero writes when LCD=ON)
  • LCD status changes:✅ LCD turns off and on, but when turned off, VRAM has only 40 non-zero bytes (0.65%)

Log Example

[MMU-VRAM-MONITOR-INIT] VRAM monitoring initialized from startup
[MMU-VRAM-INITIAL-STATE] VRAM initial state | Non-zero bytes: 5867/6144 (95.49%) | Complete tiles: 374/384 (97.40%)
[MMU-VRAM-INITIAL-STATE] ✅ VRAM has initial data (possibly from ROM)
[MMU-VRAM-WRITE-FROM-START-STATS] Total writes=1000 | Non-zero writes=0 | Non-zero ratio=0.00%
[PPU-LCD-STATE-CHANGE] Change #1 | Frame 0 | LY: 0 | LCD changed from OFF to ON
[PPU-LCD-STATE-CHANGE] Change #2 | Frame 1 | LY: 148 | LCD changed from ON to OFF
[PPU-LCD-STATE-CHANGE] LCD OFF | VRAM non-zero bytes: 40/6144 (0.65%)

Sources consulted

Educational Integrity

What I Understand Now

  • VRAM access restrictions:In real hardware, access to VRAM is restricted when the LCD is on (except during VBLANK). This prevents data corruption when the PPU is reading VRAM during rendering.
  • VRAM initial state:Games can have initial data in VRAM from ROM or from initialization. This data can be test tiles, logo tiles, or basic game tiles.
  • Identified problem:Games have initial data in VRAM (92-98% non-zero bytes), but all writes during execution are zeros (0% non-zero writes). This suggests that the initial data is being deleted before the game can load new tiles.
  • LCD Timing:The LCD turns off and on during execution, but when it turns off, VRAM already has only 40 non-zero bytes (0.65%), suggesting that the initial data was erased before the LCD turned off.

What remains to be confirmed

  • Why is the initial data deleted?We need to investigate if there are zero writes that delete the initial data before the game can load new tiles.
  • When is the initial data deleted?We need to check whether zero writes occur during initialization or during execution.
  • Do games expect VRAM to be empty?Some games may expect VRAM to be empty at startup and load tiles from scratch. We need to check if games try to load tiles but fail for some reason.
  • Are there access restrictions in place?We currently do not implement VRAM access restrictions when LCD=ON. We need to verify if this is necessary or if the problem is something else.

Hypotheses and Assumptions

Main hypothesis:Initial data in VRAM is being erased (writing 0x00) before the game can load new tiles. This could occur during game initialization or during launch when the LCD is on. If games try to load tiles when the LCD is off, but the initial data has already been erased, VRAM will be empty and nothing will be displayed on the screen.

Assumption:Games may be writing zeros to VRAM during initialization to clear VRAM before loading new tiles. If this occurs before we start monitoring, we would not detect it. However, monitoring from startup should catch these writes.

Next Steps

  • [ ] Step 0354:Investigate why the initial VRAM data is erased. Check for zero writes that occur before the game can load new tiles.
  • [ ] Step 0355:Implement VRAM access restrictions when LCD=ON (except VBLANK) if necessary.
  • [ ] Step 0356:Check if games try to load tiles but fail for some reason. Investigate if there are problems with timing or access to VRAM.