Chip-8 Book

repository·master·Indexed 43 days ago

https://github.com/aquova/chip8-book

An educational resource and tutorial for building a Chip-8 emulator from scratch using Rust, targeting both desktop and WebAssembly (Wasm). It includes the chip8_core crate (v0.1.0) featuring the Emu struct for core emulation logic and the EmuWasm class for browser-based rendering and input handling.

Tokens
16.1K
Snippets
53
Records
65
Agent score
55%

What's inside chip8-book

  1. Overview of the Chip-8 Emulation Tutorial

    master
    This project is an introductory tutorial for developing a Chip-8 emulator using the Rust programming language. It targets both desktop environments and web browsers via WebAssembly (Wasm). The tutorial is designed for developers with basic Rust knowledge and no prior emulation experience. It covers the theoretical components of emulation, the inner workings of the Chip-8 system, and provides a step-by-step implementation walkthrough.
  2. Implement Arithmetic and Bitwise Operations

    master

    Arithmetic operations often interact with the VF flag register (the 16th V register, self.v_reg[0xF]), which stores carry or borrow information.

    • VX = NN (6XNN): Set register VX to constant NN.
    • VX += NN (7XNN): Add NN to VX using wrapping_add to prevent Rust panics.
    • VX += VY (8XY4): Add VY to VX. Set VF to 1 if an overflow occurs, otherwise 0. Use overflowing_add.
    • VX -= VY (8XY5): Subtract VY from VX. Set VF to 0 if a borrow occurs (underflow), otherwise 1. Use overflowing_sub.
    • VX = VY (8XY0): Copy value from VY to VX.
    • Bitwise OR (8XY1): VX |= VY.
    • Bitwise XOR (8XY2): VX ^= VY (implied by context).
    • Bitwise AND (8XY3): VX &= VY (implied by context).
    • Right Shift (8XY6): Shift VX right by 1. Store the dropped Least Significant Bit (LSB) in VF.
    • Left Shift (8XYE): Shift VX left by 1. Store the dropped Most Significant Bit (MSB) in VF.
    • VX = VY - VX (8XY7): Subtract VX from VY and store in VX. Set VF to 1 if no borrow occurs, 0 if borrow occurs.
    // VX += VY with Carry Flag
    (8, _, _, 4) => {
        let x = digit2 as usize;
        let y = digit3 as usize;
        let (new_vx, carry) = self.v_reg[x].overflowing_add(self.v_reg[y]);
        let new_vf = if carry { 1 } else { 0 };
        self.v_reg[x] = new_vx;
        self.v_reg[0xF] = new_vf;
    }
  3. What are Chip-8 Registers?

    master

    Registers are small, named, high-speed storage locations used directly by the CPU during opcode execution.

    There are two types of registers in Chip-8:

    1. General Purpose Registers (V0-VF): Sixteen 8-bit registers named V0 through VF (or 0 through F in hex). These can be used freely by the programmer for data storage.

      • Example: The opcode 8XY4 performs VX += VY. If the opcode is 8124, it adds the value in V1 to the value in V2 and stores the result in V1.
    2. Program Counter (PC): A 16-bit register that holds the memory address (index) of the instruction currently being executed. The PC typically increments by 2 bytes after every instruction to move to the next opcode, but certain instructions can change the PC to jump to a different part of the program.

  4. Implement Subroutine Call and Return

    master

    Subroutines (functions) allow the program to jump to a specific address and later return to the original execution point. This is managed using a stack.

    • Call Subroutine (2NNN): Push the current Program Counter (self.pc) onto the stack, then set self.pc to the target address nnn.
    • Return from Subroutine (00EE): Pop the return address from the stack and set self.pc to that value.
    // CALL NNN
    (2, _, _, _) => {
        let nnn = op & 0xFFF;
        self.push(self.pc);
        self.pc = nnn;
    }
    
    // RET
    (0, 0, 0xE, 0xE) => {
        let ret_addr = self.pop();
        self.pc = ret_addr;
    }
  5. Implement Wait for Key Press (FX0A)

    master

    The FX0A instruction is a blocking operation. The emulator must pause and wait until a key is pressed.

    Implementation Strategy: To avoid blocking the entire system (which would prevent new key presses from being registered), do not use a simple while loop. Instead, check if any key is pressed; if not, decrement the Program Counter (self.pc -= 2) to re-execute the same opcode in the next fetch cycle. This allows the main loop to continue running and updating the keys array.

    When a key is found, store its index in VX.

    // WAIT KEY
    (0xF, _, 0, 0xA) => {
        let x = digit2 as usize;
        let mut pressed = false;
        for i in 0..self.keys.len() {
            if self.keys[i] {
                self.v_reg[x] = i as u8;
                pressed = true;
                break;
            }
        }
    
        if !pressed {
            // Redo opcode to allow key registration in main loop
            self.pc -= 2;
        }
    }
  6. How the Chip-8 CPU and opcodes work

    master

    The CPU is responsible for performing mathematical operations (addition, subtraction, equality checks) and control flow (jumping to different code sections, fetching/saving numbers).

    Key concepts:

    • Opcode: A unique number representing a specific mathematical or logical operation.
    • Tick/Cycle: The process of the emulator grabbing the next opcode from the ROM and executing the corresponding operation.
    • Opcode Layout: Opcodes are two bytes long. They use specific patterns to pack information. In documentation, literal hexadecimal values are represented by:
      • N: Single digit
      • NN: Two digits
      • NNN: Three digits
      • X or Y: Specifies a register index.
  7. Implement Conditional Skips

    master

    Chip-8 uses 'skip' instructions to provide if-else functionality. If the condition is met, the Program Counter (self.pc) is incremented by 2 to skip the next instruction.

    • Skip if VX == NN (3XNN): Skip if register VX equals the constant NN.
    • Skip if VX != NN (4XNN): Skip if register VX does not equal the constant NN.
    • Skip if VX == VY (5XY0): Skip if register VX equals register VY.
    • Skip if VX != VY (9XY0): Skip if register VX does not equal register VY.
    • Skip if Key Pressed (E9E): Skip if the key indexed by VX is currently pressed.
    • Skip if Key Not Pressed (E A1): Skip if the key indexed by VX is not currently pressed.
    // SKIP VX == NN
    (3, _, _, _) => {
        let x = digit2 as usize;
        let nn = (op & 0xFF) as u8;
        if self.v_reg[x] == nn {
            self.pc += 2;
        }
    }
  8. Implement Binary-Coded Decimal (FX33)

    master

    The FX33 instruction converts the 8-bit value in VX into its Binary-Coded Decimal (BCD) representation and stores the three resulting digits in RAM starting at the address in the I register.

    Example: If VX is 0x64 (decimal 100), the bytes stored in RAM[I], RAM[I+1], and RAM[I+2] will be 1, 0, and 0 respectively.

    Implementation Logic:

    • Hundreds digit: (vx / 100.0).floor()
    • Tens digit: ((vx / 10.0) % 10.0).floor()
    • Ones digit: (vx % 10.0)
    // BCD
    (0xF, _, 3, 3) => {
        let x = digit2 as usize;
        let vx = self.v_reg[x] as f32;
        let hundreds = (vx / 100.0).floor() as u8;
        let tens = ((vx / 10.0) % 10.0).floor() as u8;
        let ones = (vx % 10.0) as u8;
    
        self.ram[self.i_reg as usize] = hundreds;
        self.ram[(self.i_reg + 1) as usize] = tens;
        self.ram[(self.i_reg + 2) as usize] = ones;
    }
  9. How Chip-8 RAM and ROM interact

    master

    Chip-8 uses RAM (Random Access Memory) as a large array of numbers for the CPU to read and write.

    Memory Specifications:

    • RAM Size: While not strictly defined by the original hardware, emulators typically implement 4096 bytes (4 KB) of RAM.
    • ROM Loading: The CPU cannot read directly from the ROM file. Instead, the entire game ROM must be copied into RAM at startup.
    • Memory Offset: ROM data is not loaded at the beginning of RAM. It is offset by 512 bytes (0x200). This means the first byte of the ROM is stored at RAM address 0x200, the second at 0x201, and so on.
    • ROM Size Limit: Because the ROM is loaded into RAM, Chip-8 games are effectively limited to a maximum size of 4 KB.
  10. Interpret Chip-8 Opcode notation

    master

    Chip-8 opcodes use a specific shorthand notation to describe operations. When reading the opcode table, understand the following symbols:

    • Hexadecimal digits (0-9, A-F): Interpreted literally to determine the specific operation.
    • X or Y: Wildcards representing the index of a register (e.g., VX or VY refers to the value stored in that specific register).
    • N: A literal hexadecimal value.
    • NN: A two-digit hexadecimal value.
    • NNN: A three-digit hexadecimal value.

    Example: The instruction 0xD123 matches the DXYN opcode pattern, where VX is V1, VY is V2, and N is 3. This instruction tells the system to draw an 8x3 sprite at coordinates (V1, V2).

  11. Implement Jump and Jump to V0 + NNN

    master

    Jumps change the Program Counter (self.pc) to a new address.

    • Jump (1NNN): Set self.pc directly to the 12-bit address nnn extracted from the opcode.
    • Jump to V0 + NNN (BNNN): Set self.pc to the sum of the value in register V0 and the 12-bit address nnn provided in the opcode.
    // JMP NNN
    (1, _, _, _) => {
        let nnn = op & 0xFFF;
        self.pc = nnn;
    }
    
    // JMP V0 + NNN
    (0xB, _, _, _) => {
        let nnn = op & 0xFFF;
        self.pc = (self.v_reg[0] as u16) + nnn;
    }
  12. Understand Chip-8 ROM file structure

    master

    A Chip-8 ROM (Read-Only Memory) file contains the game logic and graphical assets required to run a game. It is a binary file consisting of a sequence of numbers, where every two bytes represent a single instruction called an opcode.

    To inspect the raw contents of a ROM, use a hex editor. In a hex editor:

    • The offset (left column) indicates the byte position from the start of the file.
    • The data values (right column) are displayed in hexadecimal.
    • Instructions are typically grouped in pairs because each opcode is two bytes long.