This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Control Flow Completion (Calls, Rets, RSTs)
Summary
This Step completes the CPU's flow control instruction set by implementing all the missing conditional and RST instructions.
Diagnostics for Step 0268 revealed that Stack Pointer was still corrupted (`SP:210A`) even after implementing stack math. The root cause was aControl Flow Disaster: If the game executes `CALL Z` or `RST 28` and they are not implemented, they act as NOPs, unbalancing the stack (a subsequent `RET` will output erroneous data) and causing the crash.
They were implemented17 new instructions: 4 conditional returns, 4 conditional calls, 4 conditional absolute jumps, 8 restarts (RST) and 1 indirect jump (JP HL). Without these instructions, the game's logic is a Gruyère cheese full of holes.
Hardware Concept
Flow control is the mechanism that allows the CPU to change the order of execution of instructions. On the Game Boy, there are several types of control flow instructions:
Conditional Returns (RET cc)
The instructions `RET NZ`, `RET Z`, `RET NC` and `RET C` return from a subroutine only if a specific condition based on the flags is met. If the condition is not met, execution continues normally.
- RET NZ (0xC0): Returns if Z=0 (Not Zero)
- RET Z (0xC8): Returns if Z=1 (Zero)
- NC RET (0xD0): Returns if C=0 (No Carry)
- RET C (0xD8): Returns if C=1 (Carry)
Timing:5 M-Cycles if fulfilled (pops and jumps), 2 M-Cycles if not (continues).
Conditional Calls (CALL cc, nn)
The `CALL NZ`, `CALL Z`, `CALL NC` and `CALL C` instructions call a subroutine only if a specific condition is met.CRITICAL:They always read `nn` (to keep the PC aligned), but only push and jump if the condition is met.
- CALL NZ, nn (0xC4): Call if Z=0
- CALL Z, nn (0xCC): Call if Z=1
- CALL NC, nn (0xD4): Call if C=0
- CALL C, nn (0xDC): Call if C=1
Timing:6 M-Cycles if fulfilled (push and jump), 3 M-Cycles if not (read nn and continue).
Conditional Absolute Jumps (JP cc, nn)
Similar to conditional calls, but without modifying the stack. They only jump if the condition is met.
- JP NZ, nn (0xC2): Jump if Z=0
- JP Z, nn (0xCA): Jump if Z=1
- JP NC, nn (0xD2): Jump if C=0
- JP C, nn (0xDA): Jump if C=1
Timing:4 M-Cycles if it jumps, 3 M-Cycles if it doesn't.
Restarts (RST n) - CRITICAL FOR POKÉMON
RST instructions are quick calls of1 bytewhich do `PUSH PC` and jump to a fixed address. They are extremely efficient and Pokémon uses them intensively for system functions (changing memory banks, managing graphics, etc.).
There are 8 RST instructions, each one jumps to a specific address:
- RST 00 (0xC7): Jump to 0x0000
- RST 08 (0xCF): Jump to 0x0008
- RST 10 (0xD7): Jump to 0x0010
- RST 18 (0xDF): Jump to 0x0018
- RST 20 (0xE7): Jump to 0x0020
- RST 28 (0xEF): Jump to 0x0028 (very used in Pokémon)
- RST 30 (0xF7): Jump to 0x0030
- RST 38 (0xFF): Jump to 0x0038
Timing:4 M-Cycles (all).
Implementation:`push_word(regs_->pc); regs_->pc = 0x00XX;`
Indirect Jump (JP HL)
The `JP (HL)` (0xE9) instruction allows you to jump to a dynamically calculated address stored in HL. It is useful for jump tables and virtual functions.
Timing:1 M-Cycle.
Why are they critical?
If these instructions are missing or poorly implemented:
- The game tries to call a vital function using `CALL NZ, aaaa`.
- If not implemented, the CPU treats that byte as an unrecognized opcode (or NOP), advances the PC,but it doesn't push anything to the Stack or jump.
- The game continues to run linearly.
- Suddenly you find a `RET` (which we do have implemented).
- `POP` the stack. But since we never did the `PUSH` of the `CALL`, we got garbage (or underflow).
- SP breaks. PC jumps to trash (210A in ROM).
Fountain:Pan Docs - "CPU Instruction Set", "Control Flow Instructions", "RST Instructions"
Implementation
17 new instructions were implemented in the methodstep()ofCPU.cpp, organized in logical sections within the switch.
Conditional Returns
case 0xC0://RETNZ
{
if (!regs_->get_flag_z()) {
uint16_t return_addr = pop_word();
regs_->pc = return_addr;
cycles_ += 5;
return 5;
} else {
cycles_ += 2;
return 2;
}
}
(Similar for RET Z, RET NC, RET C)
Conditional Calls
case 0xC4://CALL NZ, nn
{
uint16_t target = fetch_word(); // Always read nn to keep PC aligned
if (!regs_->get_flag_z()) {
uint16_t return_addr = regs_->pc;
push_word(return_addr);
regs_->pc = target;
cycles_ += 6;
return 6;
} else {
cycles_ += 3;
return 3;
}
}
Critical note:We always read `nn` even if the condition is not met, to keep the PC aligned correctly.
Conditional Absolute Jumps
case 0xC2://JP NZ, nn
{
uint16_t target = fetch_word(); // Always read nn
if (!regs_->get_flag_z()) {
regs_->pc = target;
cycles_ += 4;
return 4;
} else {
cycles_ += 3;
return 3;
}
}
Restarts (RST)
case 0xEF://RST 28 (Restart to 0x0028)
{
uint16_t return_addr = regs_->pc;
push_word(return_addr);
regs_->pc = 0x0028;
cycles_ += 4;
return 4;
}
(Similar for the other 7 RSTs: 0xC7, 0xCF, 0xD7, 0xDF, 0xE7, 0xF7, 0xFF)
Indirect Jump
case 0xE9://JP(HL)
{
uint16_t hl = regs_->get_hl();
regs_->pc = hl;
cycles_ += 1;
return 1;
}
Design Decisions
- Reading operands:In conditional instructions, we always read the operands (nn) even if the condition is not met, to keep the PC aligned correctly. This replicates the behavior of real hardware.
- Code organization:Instructions are grouped into logical sections (Returns, Calls, Jumps, RST) for easy maintenance.
- Precise timing:Each instruction returns the exact number of M-Cycles according to Pan Docs, differentiating between when the condition is met and when it is not.
Affected Files
src/core/cpp/CPU.cpp- Added 17 new flow control instructions in the methodstep():- 4 conditional returns (0xC0, 0xC8, 0xD0, 0xD8)
- 4 conditional calls (0xC4, 0xCC, 0xD4, 0xDC)
- 4 conditional absolute jumps (0xC2, 0xCA, 0xD2, 0xDA)
- 8 restarts (0xC7, 0xCF, 0xD7, 0xDF, 0xE7, 0xEF, 0xF7, 0xFF)
- 1 indirect jump (0xE9)
Tests and Verification
Compiled C++ module validation:The instructions were implemented directly in C++ and require recompilation.
Compile command:
.\rebuild_cpp.ps1
Test command:
python main.py roms/pkmn.gb
Expected verifications:
- The Stack Pointer should have "healthy" values like
DFFXeitherFFFX(in WRAM or HRAM), no210A. - The game should advance beyond the waiting loop and display new graphics on the screen.
- If this works, it is theCheckmate: Pokémon uses RST intensively to switch memory banks and drive graphics.
Note:Complete unit tests can be implemented in a future Step, following the pattern of existing tests.
Sources consulted
- Bread Docs:CPU Instruction Set - Conditional Calls
- Bread Docs:CPU Instruction Set - Conditional Returns
- Bread Docs:CPU Instruction Set - Conditional Jumps
- Bread Docs:CPU Instruction Set - RST Instructions
- Bread Docs:CPU Instruction Set - JP (HL)
Educational Integrity
What I Understand Now
- Control Flow Disaster:If a conditional instruction (such as CALL Z) is not implemented, it acts as a NOP, unbalancing the stack. When a RET is then executed, it outputs bad data and corrupts the SP.
- Reading Operands:In conditional instructions, we should always read the operands (nn) even if the condition is not met, to keep the PC aligned correctly.
- RST in Pokémon:RST instructions are critical for Pokémon, which uses them extensively for system functions such as switching memory banks and handling graphics.
What remains to be confirmed
- Validation with real ROMs:We need to run the emulator with Pokémon Red and verify that the SP is no longer corrupted and that the game is progressing correctly.
- Unit tests:Implement complete unit tests that validate the behavior of conditional statements in edge cases.
Hypotheses and Assumptions
We assume the implementation is correct based on the Pan Docs documentation. However, the final validation requires running the emulator with real ROMs and verifying that the behavior is correct. If the SP is still corrupted after this Step, we will need to investigate other possible causes (such as missing CB instructions or memory management problems).
Next Steps
- [ ] Recompile the C++ module with
.\rebuild_cpp.ps1 - [ ] Run the emulator with Pokémon Red and verify that the SP is no longer corrupted
- [ ] Verify that the game progresses past the waiting loop and displays graphics
- [ ] If the SP is still corrupted, investigate other possible causes (missing CB instructions, memory management problems, etc.)
- [ ] Implement full unit tests for the new instructions (optional, may be a future Step)