JP V0 — Computed Jumps
So far, every jump instruction has gone to a fixed address — a number baked into the opcode at the time the program was written. BNNN changes that. It adds the current value of V0 to the address, so where you land depends on what is in V0 at runtime.
This is a small change to the implementation, but a conceptually important one: the destination is no longer fixed at compile time. It can be different every time the instruction runs.
Encoding
BNNN
│└──┘
│ └─ NNN: base address
└──── opcode category B
Example: 0xB300 with V0 = 0x10 jumps to 0x310. The same instruction with V0 = 0x00 would jump to 0x300.
Decode and execute
#![allow(unused)] fn main() { 0xB => { let nnn = opcode & 0x0FFF; self.pc = nnn + self.v[0] as u16; } }
V0 must be cast to u16 before the addition — nnn is u16, and Rust does not silently promote types. Without the cast, the compiler rejects it.
Fixed vs computed: when each one makes sense
The distinction maps to a familiar programming concept:
| Opcode | Destination | Analogy |
|---|---|---|
1NNN | fixed address NNN | goto line_300 |
BNNN | NNN + V0 | goto table[index] |
BNNN is designed for jump tables. Imagine a game state machine with states 0, 1, and 2, each handled by code at a different address. Instead of a chain of conditionals, you can load the state into V0 and jump to a base address plus the offset — one instruction, any number of states.
LD V0, 2 ; state = 2
JP V0, 0x300 ; jump to 0x300 + 2 = 0x302 — handler for state 2
The handlers are laid out consecutively in memory starting at 0x300. V0 selects which one runs.
Rust concepts used here: The
as u16cast is explained in Type Casting. The opcode extraction follows the same0x0FFFmask pattern from The First Opcode: JP. See Conditional Opcodes for the other way to build branching logic in CHIP-8.