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

The Sniper: Hunting the Infinite Loop

Date:2025-12-22 StepID:0223 State: draft

Summary

"Stethoscope" (Step 0222) revealed that the CPU is stuck in an infinite loop at the memory address0x02B4, with the background off (LCDC Bit 0 = 0) and VRAM empty. To understand what output condition is not being met (probably waiting for V-Blank or a specific hardware state), we implement a conditional trace that only fires when the PC is in range0x02B0-0x02C0. This surgical instrumentation will allow us to see the loop instructions and the values ​​of the registers without saturating the console with massive logs.

Hardware Concept

When a Game Boy game starts up, it typically follows this flow:

  1. Hardware initialization:Configure I/O registers (LCDC, BGP, etc.)
  2. V-Blank Standby:Many games wait for the first V-Blank before copying graphics to VRAM
  3. Copy of graphics:Transfer tiles from ROM to VRAM
  4. Background activation:Turn on LCDC Bit 0 to show background

The problem we are facing is that the game is stuck at step 2: it is waiting for V-Blank, but our emulator is not generating V-Blank correctly, or the game is reading a hardware register (like STAT or LY) that is not updating as expected.

The recordLY (Line Y)in the direction0xFF44indicates the current scan line (0-153). When LY reaches 144, the PPU enters V-Blank mode. Many games poll this record in a loop like:

wait_vblank:
    LDH A, (0x44) ; Lee L.Y.
    CP 0x90 ; Compare with 144 (0x90)
    JR NZ, wait_vblank ; If it is not 144, wait again

If LY never reaches 144 (because the PPU is not advancing), the game gets stuck in this loop infinitely. The "Sniper" will allow us to see exactly what instructions are being executed and what values ​​they are comparing.

Implementation

We implement a conditional debug block in the methodstep()of the CPU that only prints information when the PC is in the problematic range (0x02B0-0x02C0). This allows us to see:

  • PC:The current memory address (to view the loop flow)
  • Opcode:The instruction being executed
  • AF:The AF register (contains flags and accumulator, useful for seeing comparisons)
  • LY:The current value of the scan line (critical to detect if V-Blank is working)

Modification in CPU.cpp

Added a conditional block just before the opcode fetch:

// --- Step 0223: THE SNIPER (Surgical Debug) ---
// We only print if the PC is in the hot zone of the infinite loop (0x02B0 - 0x02C0)
// This will tell us what the game is checking without overwhelming the console.
if (regs_->pc >= 0x02B0 && regs_->pc<= 0x02C0) {
    uint8_t opcode_preview = mmu_->read(regs_->pc);
    uint8_t ly_value = (ppu_ != nullptr) ? ppu_->get_ly() : 0;
    printf("[SNIPER] PC: 0x%04X | Opcode: 0x%02X | AF: 0x%04X | LY: %d\n", 
           regs_->pc, opcode_preview, regs_->get_af(), ly_value);
}
// -----------------------------------------------------

Design decisions

  • Memory range:We choose0x02B0-0x02C0because the "Stethoscope" detected that the PC was in0x02B4. We include a range to capture the entire loop, not just one statement.
  • Minimum information:We only print PC, Opcode, AF and LY. This is enough to identify the loop without overwhelming the console.
  • PPU verification:We verify thatppu_don't benullptrbefore callingget_ly()to avoid crashes.
  • No print limit:Unlike the previous trace (Step 0205) which limited it to 200 instructions, this one has no limit because it is only activated in a very small range of memory, so it will not saturate the console.

Affected Files

  • src/core/cpp/CPU.cpp- Added surgical debug block "The Sniper" in the methodstep()

Tests and Verification

To validate this implementation:

  1. Recompile:Execute.\rebuild_cpp.ps1to recompile the Cython extension with the new C++ code.
  2. Execute: python main.py roms/tetris.gb
  3. Observe output:The console should display lines like:
    [SNIPER] PC: 0x02B4 | Opcode: 0xE0 | AF: 0xXXXX | LY:XX
    [SNIPER] PC: 0x02B6 | Opcode: 0xFE | AF: 0xXXXX | LY:XX
  4. Expected analysis:
    • If we seeOpcode: 0xF0(LDH A, (n)) followed byOpcode: 0xFE(CP d8) withL.Y.constant: The game is waiting for V-Blank but LY does not advance.
    • If we seeOpcode: 0xF3(LDH A, (0x41)) followed by comparisons: The game is waiting for a specific STAT mode.
    • If we see that LY changes but the loop continues: The game is waiting for another condition (possibly an interrupt flag).

Compiled C++ module validation:The code is compiled in the Cython extension, so any compilation errors will be caught duringrebuild_cpp.ps1.

Sources consulted

Educational Integrity

What I Understand Now

  • Hardware polling:Game Boy games frequently poll hardware registers (LY, STAT) in loops to synchronize with hardware events. If these logs are not updated correctly, the game gets stuck.
  • Surgical debug:Instead of tracing all instructions (which clutters the console), we can use conditional traces that only fire in specific ranges of memory. This is more efficient and allows localized problems to be identified.
  • V-Blank as exit condition:Many games wait for V-Blank before copying graphics because it is the only "safe" time that the PPU is not reading VRAM. If V-Blank never occurs, the game cannot proceed.

What remains to be confirmed

  • LY value:We need to check if LY is advancing correctly. If LY is stuck at a value (ex: 0 or 143), we will know that the PPU is not updating the register.
  • Loop instructions:We need to see what exact instructions make up the loop. If we see LDH A, (0x44) followed by CP 0x90, we will confirm that it is waiting for V-Blank.
  • AF status:The AF register contains the flags. If we see that the Z flag changes but the loop continues, it may indicate that the exit condition is poorly implemented.

Hypotheses and Assumptions

Main hypothesis:The game is waiting for V-Blank (LY = 144) but the PPU is not updating LY correctly, or the PPU is not entering V-Blank mode. The "Sniper" will confirm if this hypothesis is correct by showing us the value of LY during the loop.

Next Steps

  • [ ] Run the emulator and collect the "Sniper" log
  • [ ] Analyze the instruction pattern to identify the exact loop
  • [ ] Check if LY is advancing or static
  • [ ] If LY does not advance: Investigate why the PPU does not update LY
  • [ ] If LY advances but the loop continues: Investigate the loop exit condition
  • [ ] Implement necessary correction based on findings