This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Sniper II: The Death Loop
Summary
The autopsy of Step 0235 revealed that the CPU has stalled at the address0x2B30after 9.5 million cycles, with the VRAM empty and the LCD off. We activate a surgical trace in that direction to identify the exact instruction and wait condition that prevents the game from continuing.
Hardware Concept
When a program stalls at a specific address for millions of cycles, it is usually waiting for a condition that is never met. This can be:
- Busy Wait Loop:A loop that reads a hardware register (such as STAT, DIV, or an I/O register) waiting for a bit to change state.
- Flag Condition:A conditional statement (JR NZ, JR Z, etc.) that jumps to itself because the flag never changes.
- Pending Interrupt:The game expects an interrupt (V-Blank, Timer, Serial) that our implementation is not generating correctly.
The autopsy analysis showed that:
- PC: 0x2B30- The CPU went from 0x02B4 to 0x2B30 (almost 10KB of code) and stopped.
- VRAM: 00- Nothing has been written to VRAM, suggesting that the game has not reached the graphics loading phase.
- IE: 0x08- Bit 3 enabled (Serial Interrupt), something unusual for Tetris boot that is normally expected by V-Blank (0x01).
- cycles_: 9,590,921- The emulator works, but the game has decided to stop.
The only way to understand what is happening is to see the exact instruction in0x2B30and associated records in real time.
Implementation
We activate a "Sniper" (surgical debug) that prints detailed information only when the PC is in the critical zone (0x2B2A - 0x2B35). This allows us to see:
- The exact opcode that is being executed.
- The next byte (in case it is a 2-byte instruction like LDH or CP).
- The complete status of the records (AF, BC, DE, HL).
Modified components
src/core/cpp/CPU.cpp: Added surgical debug block before fetch_byte() in step().src/viboy.py: Disabled Autopsy (Step 0235) to clear the console and see only the Sniper logs.
Design decisions
Debug Zone:We choose a range of addresses (0x2B2A - 0x2B35) instead of just 0x2B30 to capture instructions that may be just before or after the blocking point. This helps us understand the context of the loop.
Direct Memory Reading:We read the opcode directly from the MMU before fetch_byte() to prevent the fetch from modifying the PC before printing. This gives us an accurate view of the state at the exact moment.
Autopsy Deactivation:The autopsy generates a lot of output that can hide the Sniper logs. By disabling it, we have a clean console that shows only critical information.
Affected Files
src/core/cpp/CPU.cpp- Added#include <cstdio>and surgical debug block in step()src/viboy.py- Commented the complete Autopsy block (Step 0235)
Tests and Verification
To verify the operation of the surgical debug:
- Recompile:
.\rebuild_cpp.ps1 - Execute:
python main.py roms/tetris.gb - Notice:The logs
[SNIPER]should appear when the PC enters the 0x2B2A-0x2B35 zone
What we are looking for:
- If we see
JR NZ(relative jump if not zero) jumping to itself or a little back, it's a waiting loop. - If we see
CALLto a function that never returns, it may be a stack or return problem. - If we see readings at
0xFF00(Joypad) or0xFF01(Serial), the game is waiting for user input or serial communication. - If we see
BITeitherCPfollowed byJ.R., the game is checking a flag or register that never changes.
Sources consulted
- Bread Docs:CPU Instruction Set
- Autopsy analysis:
autopsy_step_0235_20251222_191910.txt
Educational Integrity
What I Understand Now
- Surgical Debug:Instead of logging all instructions (which is slow and generates too much output), we can activate logs only in critical areas of the code. This is similar to conditional breakpoints in a debugger.
- Busy Wait Loops:Game Boy games often wait for hardware events (V-Blank, Timer) using loops that read registers repeatedly. If our implementation doesn't raise these events correctly, the game gets stuck.
- Serial Interrupt (IE: 0x08):It is unusual for a game to enable serial interrupt during boot. This could indicate that the game is waiting for serial communication that will never arrive, or that there is a bug in our interrupt implementation.
What remains to be confirmed
- Exact Instruction:We need to see what opcode is at 0x2B30 to understand what condition it is checking.
- Flags Status:The flags (Z, N, H, C) at blocking time will tell us why a conditional statement always jumps or never jumps.
- I/O logs:If the game is reading a hardware register (STAT, DIV, Serial), we need to verify that our implementation is updating those registers correctly.
Hypotheses and Assumptions
Main Hypothesis:The game is in a wait loop checking for a hardware register (probably STAT for V-Blank or a Serial register) that our implementation is not updating correctly. The presence of IE: 0x08 (Serial) is suspicious and suggests that the game might be waiting for a serial interrupt that will never be generated.
Assumption:We assume that the opcode at 0x2B30 is a conditional instruction (JR, JP, conditional CALL) or an I/O read followed by a check. If it were an unconditional instruction (NOP, LD, etc.), the game would continue running.
Next Steps
- [ ] Run the emulator and capture the Sniper logs
- [ ] Analyze the opcode at 0x2B30 and determine what condition it is checking
- [ ] Check if the hardware register the game is reading is updating correctly
- [ ] If it is a serial interrupt, investigate why the game enables it during boot
- [ ] Implement the necessary fix based on the debug findings