ADD I, Vx — Advancing the Pointer

LD I points the index register at an address. But sometimes you need to move that pointer forward — to step through a sequence of sprites, or to store registers at consecutive locations. That is what FX1E does: it adds a register value to I, advancing the pointer by however many bytes you specify.

Encoding

The 0xF group contains several unrelated opcodes that share only their first nibble. They are distinguished by the lower byte:

FX1E
│││└─ E  ┐
││└── 1  ┘ 0x1E identifies this specific opcode within the F group
│└─── X: source register
└──── opcode category F

Decode and execute

Because the 0xF group has multiple opcodes, the implementation uses a nested match on the lower byte:

#![allow(unused)]
fn main() {
0xF => {
    let x = ((opcode & 0x0F00) >> 8) as usize;

    match opcode & 0x00FF {
        0x1E => {
            self.i += self.v[x] as u16;
        }
        0x29 => { /* LD F, Vx — set I to font sprite address */ }
        // other F opcodes...
        _ => println!("unknown: {:#06X}", opcode),
    }
}
}

self.v[x] is a u8; self.i is a u16. Rust requires an explicit cast before the addition — the types must match.

Why this is useful

Think of I as a cursor in memory. LD I places the cursor. ADD I, Vx moves it forward. Together they let you traverse memory one step at a time without hardcoding addresses.

A typical sequence might look like this:

LD   I, 0x300    ; place cursor at the start of a sprite table
ADD  I, V2       ; advance by the sprite index (V2 bytes)
DRW  V0, V1, 5  ; draw the sprite now at the cursor position

The same pair of opcodes appears when storing or loading a block of registers with FX55 and FX65 — first position I, then optionally advance it to the next block.


Rust concepts used here: The nested match on the lower byte is the same pattern used in the 0x8 group — see Pattern Matching. The as u16 cast is Type Casting. See LD I, addr for how I is initially set, and DRW — Drawing Sprites for the most common reason to adjust it.