This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
CPU: Implementation of Conditional Relative Jumps
Summary
After implementing the compare statementCP d8(Step 0161), the emulator still had the deadlock symptom (LY=0), indicating that the CPU had found another unimplemented opcode immediately after the comparison. The most likely cause was a conditional jump instruction that the game uses to make decisions based on the results of comparisons. The complete family of conditional relative jumps has been implemented:JR Z, e(0x28),JR NC, e(0x30) andJR C, e(0x38), thus completing the basic flow control capability of the CPU.
Hardware Concept
The instructions ofconditional relative jumpThey are the mechanism that allows the CPU to make decisions based on the results of previous operations (comparisons, arithmetic, etc.). They are the basic "brain" of any program:
- JR Z, e(Jump Relative if Zero): Jumps if the Z flag is activated (Z=1), indicating that the result of the last operation was zero or that two values were equal.
- JR NZ, e(Jump Relative if Not Zero): It was already implemented. Jump if Z=0.
- JR C, e(Jump Relative if Carry): Jumps if the C flag is activated (C=1), indicating that there was an overflow (carry) or that one value was less than another in a comparison.
- JR NC, e(Jump Relative if No Carry): Jumps if C=0, indicating that there was no carry or that one value was greater than or equal to another.
Why are they critical after CP?The typical sequence in game code is:
CP 0x0A- Compare A with 10, update flagsJR Z, label- If A == 10, jump to "label"- Alternative code if A != 10
Conditional Timing:All of these instructions consume different amounts of cycles depending on whether or not the jump is taken:
- 3 M-Cyclesif jump is taken (condition is true)
- 2 M-Cyclesif the jump is NOT taken (condition is false)
This timing difference is critical for accurate emulator synchronization. The real hardware always reads the offset (to maintain PC consistency), but only executes the jump if the condition is true.
The Inescapable Logic:After implementingCP d8, the game could ask questions (compare values), but could not react to answers (conditionally skip). This caused the CPU to be left executing code that had no way of making decisions, resulting in a logical deadlock.
Reference:Pan Docs - CPU Instruction Set, sections "JR Z, e" (0x28), "JR NC, e" (0x30) and "JR C, e" (0x38).
Implementation
Opcodes implemented0x28 (JR Z), 0x30 (JR NC)and0x38 (JR C)on the C++ CPU, following the same pattern as the existing implementation ofJR NZ(0x20).
Created/Modified Components
- CPU.cpp:Added cases
0x28,0x30and0x38in the opcode switch. - test_core_cpu_jumps.py:Added three new types of tests (
TestJumpRelativeConditionalZ,TestJumpRelativeConditionalC) with 6 additional tests to validate all new instructions.
Implementation of Opcodes
The three opcodes follow the same pattern asJR NZ:
case 0x28://JR Z, e (Jump Relative if Zero)
{
uint8_t offset_raw = fetch_byte();
if (regs_->get_flag_z()) {
// True condition: jump
int8_t offset = static_cast<int8_t>(offset_raw);
uint16_t new_pc = (regs_->pc + offset) & 0xFFFF;
regs_->pc = new_pc;
cycles_ += 3; // JR Z consumes 3 M-Cycles if he jumps
return 3;
} else {
//False condition: do not jump, continue normal execution
cycles_ += 2; // JR Z consumes 2 M-Cycles if he doesn't jump
return 2;
}
}
case 0x30: // JR NC, e (Jump Relative if No Carry)
{
uint8_t offset_raw = fetch_byte();
if (!regs_->get_flag_c()) {
// True condition: jump
int8_t offset = static_cast<int8_t>(offset_raw);
uint16_t new_pc = (regs_->pc + offset) & 0xFFFF;
regs_->pc = new_pc;
cycles_ += 3; // JR NC consumes 3 M-Cycles if it jumps
return 3;
} else {
// False condition: do not jump
cycles_ += 2; // JR NC consumes 2 M-Cycles if it does not jump
return 2;
}
}
case 0x38: // JR C, e (Jump Relative if Carry)
{
uint8_t offset_raw = fetch_byte();
if (regs_->get_flag_c()) {
// True condition: jump
int8_t offset = static_cast<int8_t>(offset_raw);
uint16_t new_pc = (regs_->pc + offset) & 0xFFFF;
regs_->pc = new_pc;
cycles_ += 3; // JR C consumes 3 M-Cycles if he jumps
return 3;
} else {
// False condition: do not jump
cycles_ += 2; // JR C consumes 2 M-Cycles if it does not jump
return 2;
}
}
Design Decisions
Consistent pattern:Exactly the same pattern was followed asJR NZto maintain code consistency and facilitate future maintenance.
Precise timing:Each instruction respects the conditional timing of the real hardware: 3 cycles if the jump is taken, 2 cycles if it is not taken. This is critical for proper emulation timing.
Offset conversion:The same cast mechanism is useduint8_ttoint8_tthat inJR NZ, taking advantage of C++'s native two's complement representation to handle negative offsets.
Affected Files
src/core/cpp/CPU.cpp- Added cases0x28,0x30and0x38in the opcode switch, just after0x20 (JR NZ).tests/test_core_cpu_jumps.py- Added test classesTestJumpRelativeConditionalZandTestJumpRelativeConditionalCwith 6 new tests.
Tests and Verification
6 new tests were added to the existing filetests/test_core_cpu_jumps.py, covering all possible cases for the three new instructions.
- Command executed:
pytest tests/test_core_cpu_jumps.py -v - Expected result:All tests passed (existing tests + 6 new ones)
Test code (key fragment - JR Z):
def test_jr_z_taken(self):
"""Check JR Z, e when the jump is taken (Z=1)."""
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x0100
# Activate flag Z (bit 7 of F = 1)
regs.f = regs.f | 0x80
# Write JR Z +10 (0x28 0x0A)
mmu.write(0x0100, 0x28) # Opcode JR Z, e
mmu.write(0x0101, 0x0A) # Offset +10
cycles = cpu.step()
# Must jump (Z=1, condition true)
assert regs.pc == 0x010C, (
f"PC must be 0x010C after JR Z +10 (Z=1), it is 0x{regs.pc:04X}"
)
assert cycles == 3, f"JR Z must consume 3 M-Cycles when jumping, consumed {cycles}"
def test_jr_z_not_taken(self):
"""Check JR Z, e when the jump is NOT taken (Z=0)."""
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x0100
# Disable flag Z
regs.f = regs.f & 0x7F
# Write JR Z +10 (0x28 0x0A)
mmu.write(0x0100, 0x28) # Opcode JR Z, e
mmu.write(0x0101, 0x0A) # Offset +10
cycles = cpu.step()
# should NOT jump (Z=0, false condition)
assert regs.pc == 0x0102, (
f"PC must be 0x0102 after JR Z +10 (Z=0, does not jump), it is 0x{regs.pc:04X}"
)
assert cycles == 2, f"JR Z must consume 2 M-Cycles when NOT jumping, consumed {cycles}"
Native Validation:The tests validate the compiled C++ module directly, verifying that all instructions work correctly with precise timing and correct flag handling.
Verification in Emulator:When runningpython main.py roms/tetris.gb, the emulator should overcome the deadlockLY=0and start moving forward. One of two results is expected:
- Success!The deadlock disappears and
L.Y.begins to increase, indicating that the CPU has passed the entire initialization phase. - Progress:The emulator advances and encounters the following unimplemented opcode, which will be reported by the case warning
default.
Sources consulted
- Pan Docs: CPU Instruction Set -JR Z, e (opcode 0x28)
- Pan Docs: CPU Instruction Set -JR NC, e (opcode 0x30)
- Pan Docs: CPU Instruction Set -JR C, e (opcode 0x38)
- Pan Docs: CPU Instruction Set - Flags (Z, C) -CPU Registers and Flags
Educational Integrity
What I Understand Now
- Basic Flow Control:Conditional jump instructions are the fundamental mechanism that allows any program to make decisions. Without them, a program can only run linearly, without the ability to react to different conditions.
- The CP + JR Pattern:The "compare then conditionally jump" sequence is the most common pattern in low-level code. It allows implementing high-level control structures (if/else, while, for) in machine code.
- Conditional Timing:The fact that conditional branch instructions consume different numbers of cycles depending on whether the branch is taken or not is a feature of real hardware that must be faithfully replicated to achieve accurate timing.
- Flags as Global State:The flags (Z, C, N, H) are global state shared between all instructions. A comparison updates the flags, and the next conditional jump instruction reads them to make a decision.
What I Learned from the Debugging Process
- "Peel the Onion":The debugging process is iterative. Every time we solve one problem, we reveal the next. This is not a failure, it is the natural process of incremental building.
- The Inescapable Logic:When a symptom persists (such as
LY=0), but we know that we have corrected something, it means that we have taken a step forward and hit the next barrier. This is progress, not stagnation. - Thinking like a 1990s Programmer:After a comparison, the next logical action is a conditional jump. Thinking in terms of "What does the code need to work?" guides us towards the correct implementations.
- Instruction Families:Conditional jump instructions form a logical family. Implementing them all together makes sense because they share the same structure and behavior pattern.
What remains to be understood
- Conditional Absolute Jumps:In addition to conditional relative jumps (JR), there are conditional absolute jumps (JP Z, JP NZ, JP C, JP NC) that jump to 16-bit addresses. These will be needed for more complex flow control.
- CALL and RET Conditionals:Calls to subroutines and returns can also be conditional, allowing functions to be implemented that are only executed under certain conditions.
- Compiler Optimizations:Modern compilers generate machine code that takes advantage of these instructions very efficiently. Understanding how they are used in practice will help us optimize our emulator.
Next Steps
- Recompile and Run:Recompile the C++ module with
.\rebuild_cpp.ps1and run the emulator to verify that the deadlock is resolved. - Monitor Progress:Observe if
L.Y.begins to increase, indicating that the CPU is working correctly. - Identify Next Opcode:If another unimplemented opcode warning appears, identify it and implement it in the next step.
- Implement More Hops:Consider implementing conditional absolute jumps (JP Z, JP NZ, JP C, JP NC) if they are necessary for progress.
- Validate with Tests:Make sure all tests pass correctly before continuing.