diff --git a/src/machine/cpu.odin b/src/machine/cpu.odin index 339c32e..1dfe2ce 100644 --- a/src/machine/cpu.odin +++ b/src/machine/cpu.odin @@ -5,188 +5,158 @@ import "core:log" // CPU.odin -> instructions, fetch, decode, execute, run loop cycle :: proc(s: ^System) { - program_counter := s.pc + // Fetch + high_byte := s.memory[s.pc] + low_byte := s.memory[s.pc + 1] + s.pc += 2 - // Fetch - // --------------- - high_byte := s.memory[program_counter] - low_byte := s.memory[program_counter + 1] + // Get full opcode from the 2 bytes + opcode := (u16(high_byte) << 8) | u16(low_byte) - // advance past this instruction early for our next opcode - s.pc += 2 + // Pre-decode common components of opcodes + first_nibble := (opcode & 0xF000) >> 12 + vx_idx := (opcode & 0x0F00) >> 8 + vy_idx := (opcode & 0x00F0) >> 4 + last_nibble := (opcode & 0x000F) + kk := u8(opcode & 0x00FF) + nnn := (opcode & 0x0FFF) - // Instructions are made of 2 bytes we combine the high and low byte - opcode := (u16(high_byte) << 8) | u16(low_byte) - - // mask off everything except the first byte. - // shift it 12 bits 'back' left with the first number - kind := (opcode & 0xF000) >> 12 - - // Decode + Execute - // 1. opcodes are broken up to get their particulars - // 2. first nibble (4bits half byte) is the first hex number, this is the category - // --------------- - switch kind { - case 0x0: - switch opcode { - case 0x00E0: op_cls(s) - case 0x00EE: op_ret(s, opcode) - } - case 0x1: op_jp_add(s, opcode) - case 0x2: op_call_add(s, opcode) - case 0x3: op_skip_on_condition(s, opcode, true) - case 0x4: op_skip_on_condition(s, opcode, false) - case 0x5: op_skip_reg_condition(s, opcode, true) - case 0x6: op_ld(s, opcode) - case 0x7: op_add(s, opcode) - - // todo 0x8 with last bit for ALU - - case 0x9: op_skip_reg_condition(s, opcode, false) - case 0xA: op_set(s, opcode) - case 0xD: op_draw(s, opcode) - } + // Decode + Execute + switch first_nibble { + case 0x0: + switch opcode { + case 0x00E0: op_cls(s) + case 0x00EE: op_ret(s) + } + case 0x1: op_jp_addr(s, nnn) + case 0x2: op_call_addr(s, nnn) + case 0x3: op_skip_on_condition(s, vx_idx, kk, true) + case 0x4: op_skip_on_condition(s, vx_idx, kk, false) + case 0x5: op_skip_reg_condition(s, vx_idx, vy_idx, true) + case 0x6: op_ld(s, vx_idx, kk) + case 0x7: op_add(s, vx_idx, kk) + case 0x8: + switch last_nibble { + case 0x0: op_alu_ld(s, vx_idx, vy_idx) + // case 0x1: op_alu_or(s, vx_idx, vy_idx) + } + case 0x9: op_skip_reg_condition(s, vx_idx, vy_idx, false) + case 0xA: op_set(s, nnn) + case 0xB: op_jp_offset(s, nnn) + case 0xD: op_draw(s, vx_idx, vy_idx, last_nibble) + } } debug_opcode :: proc(opcode: u16) { - log.debugf("opcode: 0x%04X", opcode) + log.debugf("opcode: 0x%04X", opcode) } -// Instruction procs // ----------------------------------- -// CLS: Clear the display -op_cls :: proc(s: ^System) { - // log.info("found clear screen instruction, zero out display buffer") +// Instruction Implementations +// ----------------------------------- - // Odin lets you zero a fixed array with just: {} is the zero value, - // so it clears the entire buffer in one shot. - s.display = {} +// 00E0 - CLS +op_cls :: proc(s: ^System) { + s.display = {} } -// RET: Return from a subroutine. -op_ret :: proc(s: ^System, opcode: u16) { - // decrement sp to the previous used slot - // current sp will always be empty +// 00EE - RET +op_ret :: proc(s: ^System) { s.sp -= 1 - - // restore the pc from where we called from s.pc = s.stack[s.sp] } -// JP addr: Jump to location nnn -op_jp_add :: proc(s: ^System, opcode: u16) { - addr := (opcode & 0x0FFF) - s.pc = addr +// 1nnn - JP addr +op_jp_addr :: proc(s: ^System, nnn: u16) { + s.pc = nnn } -// Call addr: Call subroutine at nnn. -op_call_add :: proc(s: ^System, opcode: u16) { - addr := (opcode & 0x0FFF) - - // Save current pc to stack for return location +// 2nnn - CALL addr +op_call_addr :: proc(s: ^System, nnn: u16) { s.stack[s.sp] = s.pc - // advance sp for next free slot s.sp += 1 - - s.pc = addr + s.pc = nnn } -// SE: Skip next instruction if Vx = kk. -// SNE: Skip next instruction if Vx != kk. -op_skip_on_condition :: proc(s: ^System, opcode: u16, condition: bool) { - // Extract the register indices to get values - vx_index := (opcode & 0x0F00) >> 8 - register_val := s.v[vx_index] +// 3xkk - SE Vx, byte | 4xkk - SNE Vx, byte +op_skip_on_condition :: proc(s: ^System, vx_idx: u16, kk: u8, condition: bool) { + vx_val := s.v[vx_idx] - // Extract value to compare - op_val := u8(opcode & 0x00FF) - - if (register_val == op_val) == condition { + if (vx_val == kk) == condition { s.pc += 2 } } -// SE Vx, Vy: Skip next instruction if Vx = Vy -// SNE Vx, Vy: Skip next instruction if Vx != Vy. -op_skip_reg_condition :: proc(s: ^System, opcode: u16, condition: bool) { - // Extract the register indices - vx_index := (opcode & 0x0F00) >> 8 - vy_index := (opcode & 0x00F0) >> 4 +// 5xy0 - SE Vx, Vy | 9xy0 - SNE Vx, Vy +op_skip_reg_condition :: proc(s: ^System, vx_idx, vy_idx: u16, condition: bool) { + vx_val := s.v[vx_idx] + vy_val := s.v[vy_idx] - // Read the actual value from the registers. - rx := s.v[vx_index] - ry := s.v[vy_index] - - if (rx == ry) == condition { + if (vx_val == vy_val) == condition { s.pc += 2 } } - -// continue Bnnn - -// LD: Load value kk into x register -op_ld :: proc(s: ^System, opcode: u16) { - register := (opcode & 0x0F00) >> 8 - value := (opcode & 0x00FF) - - s.v[register] = u8(value) +// Bnnn - JP V0, addr +// @TODO: add configurable quirk for chip-48 +op_jp_offset :: proc(s: ^System, nnn: u16) { + s.pc = nnn + s.pc += u16(s.v[0]) } -// ADD: Adds the value kk to the value of register Vx, then stores the result in Vx. -op_add :: proc(s: ^System, opcode: u16) { - register := (opcode & 0x0F00) >> 8 - value := (opcode & 0x00FF) - s.v[register] += u8(value) +// 6xkk - LD Vx, byte +op_ld :: proc(s: ^System, vx_idx: u16, kk: u8) { + s.v[vx_idx] = kk } -// SET: The value of register I is set to nnn. -op_set :: proc(s: ^System, opcode: u16) { - index_addr := (opcode & 0x0FFF) - s.i = index_addr +// 7xkk - ADD Vx, byte +op_add :: proc(s: ^System, vx_idx: u16, kk: u8) { + s.v[vx_idx] += kk } -// DRAW: Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision. -op_draw :: proc(s: ^System, opcode: u16) { - // Extract the register indices that hold the sprite's X and Y screen position - vx_index := (opcode & 0x0F00) >> 8 - vy_index := (opcode & 0x00F0) >> 4 - - // Read the actual coordinates from those registers. - // Wrap to screen bounds: 64 wide (0x3F mask), 32 tall (0x1F mask) - screen_x := s.v[vx_index] & 0x3F - screen_y := s.v[vy_index] & 0x1F - - // VF is the collision flag — clear it before drawing - s.v[0xF] = 0 - - // Number of rows in the sprite (1 byte per row, each byte = 8 pixels) - sprite_height := opcode & 0x000F - - for row in 0..> u8(col) - - // Only draw if this bit in the sprite is set - if sprite_row_bits & bit_mask != 0 { - // Wrap the draw position if the sprite goes off-screen - pixel_x := (screen_x + u8(col)) % 64 - pixel_y := (screen_y + u8(row)) % 32 - - pixel := &s.display[pixel_y][pixel_x] - - // CHIP-8 XORs pixels — if we're about to unset a lit pixel, that's a collision - if pixel^ == 1 { - s.v[0xF] = 1 - } - - // XOR the pixel: toggles it on if off, off if on - pixel^ ~= 1 - } - } - } +// 8xy0 - LD Vx, Vy +op_alu_ld :: proc(s: ^System, vx_idx, vy_idx: u16) { + s.v[vx_idx] = s.v[vy_idx] +} + +// Annn - LD I, addr +op_set :: proc(s: ^System, nnn: u16) { + s.i = nnn +} + +// Dxyn - DRW Vx, Vy, nibble +op_draw :: proc(s: ^System, vx_idx, vy_idx, n: u16) { + // Read positions and apply bounds masks (64 wide, 32 tall) + screen_x := s.v[vx_idx] & 0x3F + screen_y := s.v[vy_idx] & 0x1F + + // Clear collision flag (VF) before drawing + s.v[0xF] = 0 + + for row in 0..> u8(col) + + // Only process and draw if the sprite bit is active (1) + if sprite_row_bits & bit_mask != 0 { + // Wrap pixel coordinates if they go over screen bounds + pixel_x := (screen_x + u8(col)) % 64 + pixel_y := (screen_y + u8(row)) % 32 + + pixel := &s.display[pixel_y][pixel_x] + + // If target pixel is already lit, we have a collision (XOR will turn it off) + if pixel^ == 1 { + s.v[0xF] = 1 + } + + // Toggle pixel state via XOR + pixel^ ~= 1 + } + } + } }