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:

OpcodeDestinationAnalogy
1NNNfixed address NNNgoto line_300
BNNNNNN + V0goto 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 u16 cast is explained in Type Casting. The opcode extraction follows the same 0x0FFF mask pattern from The First Opcode: JP. See Conditional Opcodes for the other way to build branching logic in CHIP-8.