DMG-01 Gameboy Emulator

repository·master·Indexed 42 days ago

https://github.com/rylev/dmg-01

An educational Gameboy Emulator implemented in Rust and available as a JS browser emulator. Designed as an accessible learning resource, it prioritizes code readability and educational value over cycle-perfect accuracy. The project includes a CPU implementation (version 0.0.1) and documentation on Game Boy architecture, including memory management, bit manipulation, and instruction execution.

Tokens
26.5K
Snippets
89
Records
117
Agent score
53%

What's inside DMG-01

  1. Overview of DMG-01

    master
    DMG-01 is an educational Gameboy Emulator written in Rust. Unlike high-accuracy emulators such as mooneye-gb or BGB, the primary design goal of DMG-01 is to be easy to understand and easy to use for educational purposes. It aims to run most major Game Boy games while prioritizing code readability and educational value over cycle-perfect accuracy.
  2. Use Bit Shifting (<< >>) to move bit positions

    master

    Bit shifting moves bits to different digit positions.

    • Left Shift (<<): Moves bits to the left. Empty positions on the right are filled with 0s. Bits shifted off the left edge disappear.
    • Right Shift (>>): Moves bits to the right. Empty positions on the left are filled with 0s. Bits shifted off the right edge disappear.

    Bit shifting is frequently used to extract specific bytes from a multi-byte number. For example, to get the most significant byte of a 32-bit value, you can shift it right by 16 bits.

    // Right shifting to extract the most significant byte
    0x1c74a3 >> 16 == 0x1c
    
    // Examples of right shifting
    1001 >> 1 == 0100
    1001 >> 2 == 0010
    
    // Examples of left shifting
    1101 << 1 == 1010
    1101 << 2 == 0100
  3. Handle Prefix Instructions (0xCB)

    master

    Approximately half of the Game Boy's instructions are Prefix Instructions. These are identified by a specific prefix byte, 0xCB.

    When the CPU fetches a byte and detects it is 0xCB, it must:

    1. Recognize the prefixed state.
    2. Fetch the next byte from memory (at pc + 1).
    3. Decode this second byte using a specialized prefix-aware decoding function (e.g., Instruction::from_byte_prefixed).

    This effectively makes the instruction 2 bytes wide instead of 1.

    impl CPU {
      fn step(&mut self) {
        let mut instruction_byte = self.bus.read_byte(self.pc);
        let prefixed = instruction_byte == 0xCB;
        
        if prefixed {
          // Read the actual instruction byte following the prefix
          instruction_byte = self.bus.read_byte(self.pc + 1);
        }
    
        let next_pc = if let Some(instruction) = Instruction::from_byte(instruction_byte, prefixed) {
          self.execute(instruction)
        } else {
          let description = format!("0x{}{:x}", if prefixed { "cb" } else { "" }, instruction_byte);
          panic!("Unkown instruction found for: {}", description)
        };
    
        self.pc = next_pc;
      }
    }
  4. Use Bitwise AND (&) for bit masking

    master

    The bitwise AND (&) operator compares each bit of two numbers. A bit in the result is set to 1 only if both corresponding bits in the operands are 1.

    This is commonly used for bit masking: isolating a specific bit or byte within a larger value by ANDing it with a mask that has 0s in all positions except the ones you want to extract.

      1001
    & 1100
    ------
      1000
  5. Understand bits and bytes in the Game Boy architecture

    master

    The Game Boy is an 8-bit machine. This means it typically processes data in chunks of 8 bits, known as a byte.

    • Bit: The smallest unit of data, representing a 0 or 1.
    • Byte: A collection of 8 bits. A single byte can represent $2^8$ (256) distinct values, ranging from 0 to 255.
    • Nibble: A half-byte, consisting of 4 bits.
    • 16-bit instructions: While the machine is 8-bit, it can execute instructions that act on two bytes (16 bits) simultaneously.

    In memory, each compartment typically stores one byte.

  6. Understand the Program Counter (PC) and Memory Addressing

    master

    The CPU uses a 16-bit Program Counter (PC) to track the address of the instruction currently being executed. This PC allows the CPU to address the Game Boy's memory, which is a large array of 65,536 (0xFFFF) 8-bit numbers.

    Key memory regions include:

    • 0x0000 to 0x00FF: Hard-coded bootstrap instructions and splash screen data.
    • 0x0100 to 0x3FFF: Cartridge contents (Game ROM).

    In this model, the PC acts as the index into the memory array to fetch the next instruction byte.

    struct CPU {
      registers: Registers,
      pc: u16,
      bus: MemoryBus,
    }
    
    struct MemoryBus {
      memory: [u8; 0xFFFF]
    }
    
    impl MemoryBus {
      fn read_byte(&self, address: u16) -> u8 {
        self.memory[address as usize]
      }
    }
  7. Use Bitwise OR (|) to combine values

    master

    The bitwise OR (|) operator compares each bit of two numbers. A bit in the result is set to 1 if at least one of the corresponding bits in the operands is 1.

    Bitwise OR is useful for combining binary values or ensuring specific bits are set. For example, to ensure the least significant bit of a value is set to 1, you can OR the value with 0b1.

      1001
    | 1100
    ------
      1101
  8. Understand and implement CPU cycles for instruction execution

    master

    In the DMG-01 emulation, time is measured in CPU cycles rather than nanoseconds. Each instruction takes a specific number of cycles to complete. To synchronize the system, the execute method must return both the next Program Counter (pc) value and the number of cycles consumed by the instruction.

    For example, an ADD instruction targeting register C takes 4 cycles. The Game Boy's clock speed is 4.19 MHz (4,190,000 cycles per second).

    impl CPU {
      fn execute(&mut self, instruction: Instruction) -> (u16, u8) {
        match instruction {
          Instruction::ADD(target) => {
            match target {
              ArithmeticTarget::C => {
                // Returns (next_pc, cycles_consumed)
                (self.pc.wrapping_add(1), 4)
              }
              _ => { panic!("TODO: support more targets") }
            }
          }
          _ => { panic!("TODO: support more instructions") }
        }
      }
    }
  9. Configure flags for arithmetic operations

    master

    When performing arithmetic like ADD, the CPU must update the FlagsRegister. The four primary flags are:

    • Zero: true if the result of the operation is 0.
    • Subtract: true if the operation was a subtraction.
    • Carry: true if the operation resulted in an overflow (detected via overflowing_add).
    • Half Carry: true if there was a carry from the lower nibble (bits 0-3) to the upper nibble (bits 4-7). This can be calculated by checking if (A & 0xF) + (value & 0xF) > 0xF.
    impl CPU {
      fn add(&mut self, value: u8) -> u8 {
        let (new_value, did_overflow) = self.registers.a.overflowing_add(value);
        self.registers.f.zero = new_value == 0;
        self.registers.f.subtract = false;
        self.registers.f.carry = did_overflow;
        // Half Carry is set if adding the lower nibbles of the value and register A
        // together result in a value bigger than 0xF.
        self.registers.f.half_carry = (self.registers.a & 0xF) + (value & 0xF) > 0xF;
        new_value
      }
    }
  10. Understand the DMG-01 Memory Map

    master
    The DMG-01 (Game Boy) memory is not a single contiguous array of RAM. Instead, it is a memory-mapped system where specific address ranges are tied to different hardware components, such as the screen, audio device, or ROM banks. While the CPU treats memory as a 0xFFFF entry array, developers must account for the specific purpose of each memory segment to interact with hardware correctly.