Major clean before doing the rest of instructions.

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