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

Debug: CPU Trace to Diagnose Empty VRAM

Date:2025-12-19 StepID:0149 State: 🔍 DRAFT

Summary

After solving theSegmentation Faultand get the emulator to run stable at 60 FPS, the next problem identified is ablank screen. The diagnostic indicates that VRAM is empty because the CPU is not running the routine that copies graphics data from ROM to VRAM. Added diagnostic instrumentation inCPU::step()to plot the first 100 instructions executed by the ROM, showing the PC (Program Counter) and the opcode of each instruction. This trace will allow you to identify which instruction is missing or which loop is blocking execution.

Hardware Concept

On the Game Boy, when a ROM is booted, the CPU executes a sequence of instructions that initializes the hardware and copies the graphics data (tiles, sprites, maps) from the ROM (read-only memory) to the VRAM (Video RAM, range0x8000-0x9FFF). The PPU (Picture Processing Unit) reads this data from VRAM to render the screen.

The white screen problem:If the screen is blank but the emulator runs at 60 FPS, it means that:

  • Heframebufferit is being created and passed to Pygame correctly
  • The Python renderer is drawing the content of theframebuffer
  • The content offramebufferis uniformly the background color (color index 0, which our default palette translates to white)

This indicates that the PPU is rendering correctly, but is reading aVRAM that is completely empty (full of zeros). The VRAM is empty because the CPU has not yet executed the code routine that copies the Nintendo logo graphics data from the ROM to the VRAM.

Why would the VRAM be empty?The CPU is executing code, but it is probably stuck in a loop or missing a key instruction preventing it from reaching the graphics copy routine. To diagnose this, we need to see exactly what instructions the CPU is executing and at what point it stops or enters an infinite loop.

The Solution: Instruction LayoutAdding diagnostic logs in the methodCPU::step()that show the PC (Program Counter) and the opcode of each instruction before executing it, we can see the exact flow of execution. By limiting the number of logs to the first 100 instructions, we obtain enough information to identify the problem without overwhelming the console.

Implementation

Added diagnostic instrumentation insrc/core/cpp/CPU.cppto trace the first 100 instructions executed by the CPU. The log shows the instruction counter, the PC (Program Counter) before the opcode was read, and the opcode read.

Modified components

  • src/core/cpp/CPU.cpp: Added#include <cstdio>, static variables for debug counter, and logging block instep()

Changes applied

1. I/O library inclusion:

  • Added#include <cstdio>at the beginning of the file to useprintf

2. Static variables for logging:

  • Added static variabledebug_instruction_counterto count logged instructions
  • constant addedDEBUG_INSTRUCTION_LIMIT = 100to limit the number of logs
  • The counter is reset to 0 in the constructorCPUfor each new instance

3. Record block instep():

  • The current PC is saved before reading the opcode (becausefetch_byte()increases PC)
  • A conditional block is added that prints the counter, PC and opcode if the counter is less than the limit
  • The log format is:[CPU TRACE N] PC: 0xXXXX | Opcode: 0xXX

Key code

Added static variables:

// Static variables for diagnostic logging
static int debug_instruction_counter = 0;
static const int DEBUG_INSTRUCTION_LIMIT = 100;

Reset the counter in the constructor:

CPU::CPU(MMU* mmu, CoreRegisters* registers)
    : mmu_(mmu), regs_(registers), cycles_(0), ime_(false), halted_(false), ime_scheduled_(false) {
    //...
    // Reset debug counter when creating new instance
    debug_instruction_counter = 0;
}

Logging block instep():

// ========== PHASE 4: Fetch-Decode-Execute ==========
// Fetch: Read opcode from memory
uint16_t current_pc = regs_->pc;  // We save the current PC for the log
uint8_t opcode = fetch_byte();

// --- DIAGNOSTIC LOGING BLOCK ---
if (debug_instruction_counter< DEBUG_INSTRUCTION_LIMIT) {
    printf("[CPU TRACE %d] PC: 0x%04X | Opcode: 0x%02X\n",
           debug_instruction_counter, current_pc, opcode);
    debug_instruction_counter++;
}
// --- FIN DEL BLOQUE DE LOGGING ---

Affected Files

  • src/core/cpp/CPU.cpp- Added#include <cstdio>, static variables for logging, and logging block instep()

Tests and Verification

To verify instrumentation:

  1. Recompile the module:Execute.\rebuild_cpp.ps1(eitherpython setup.py build_ext --inplace)
  2. Run the emulator:Executepython main.py roms/tetris.gb
  3. Parse the output:The console will display the first 100 executed instructions in the format:
    [CPU TRACE 0] PC: 0x0100 | Opcode: 0x31
    [CPU TRACE 1] PC: 0x0103 | Opcode: 0xAF
    [CPU TRACE 2] PC: 0x0104 | Opcode: 0x21
    ...
    [CPU TRACE 99] PC: 0xXXXX | Opcode: 0xXX
  4. Identify the problem:Search in the trace:
    • The last opcode executed before the CPU enters a loop
    • An unimplemented opcode (which would return 0 cycles and cause an infinite loop)
    • An obvious infinite loop (same PC and opcode repeated)

Note:This instrumentation is temporary and will be removed after the problem is identified to restore maximum performance.

Sources consulted

Note: Diagnostic instrumentation is a standard low-level debugging technique. The log format follows common CPU trace conventions.

Educational Integrity

What I Understand Now

  • Layout instructions:Instruction tracing is a fundamental debugging technique that shows the exact flow of execution of a program. In an emulator, this is especially useful because we can see exactly what instructions the ROM is executing and at what point it stops or loops.
  • White Screen vs Segmentation Fault:A white screen at 60 FPS indicates that the emulator is working correctly at the timing level, but the CPU is not executing the code necessary to initialize the VRAM. This is a different and more predictable problem than a Segmentation Fault.
  • Static variables in C++:Static variables in C++ have a file scope and are initialized only once. They are useful for global counters that must persist between function calls, but should be used with care in multi-threaded contexts.

What remains to be confirmed

  • Missing opcode:We need to run the emulator and analyze the trace to identify which opcode is missing or which loop is blocking execution.
  • Initialization routine:Once the missing opcode is identified, we will need to implement it and verify that the CPU can continue to the graphics copy routine.

Hypotheses and Assumptions

Main hypothesis:The CPU is encountering an opcode that we haven't ported to C++ yet and that is essential for initialization. This opcode probably returns 0 cycles (because it is in thedefaultof the switch), causing the CPU to not advance and enter an infinite loop or stop.

Assumption:The problem is not in the PPU or the MMU (both are working correctly), but in the CPU that cannot execute all the instructions necessary to initialize the VRAM.

Next Steps

  • [ ] Recompile the C++ module with instrumentation
  • [ ] Run the emulator and capture the trace of the first 100 instructions
  • [ ] Analyze the trace to identify the last opcode executed or the infinite loop
  • [ ] Implement missing opcode or fix loop
  • [ ] Verify that the CPU can continue to the graphics copy routine
  • [ ] Remove diagnostic instrumentation to restore performance