const Self = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const Errors = error { UnknownInstruction, ProgramCounterOutOfBounds, MemoryOutOfBounds, StackOverflow, StackUnderflow }; allocator: Allocator, display: []bool, display_width: u8, display_height: u8, memory: []u8, stack: [16]u16, rng: std.rand.DefaultPrng, input: [16]bool, V: [16]u8, I: u16 = 0, // Address pointer PC: u16 = 0, // Program counter SP: u8 = 0, // Stack pointer DT: u8 = 0, // Delay timer ST: u8 = 0, // Sound timer fn get_inst_x(inst: u16) u4 { return @truncate((inst & 0x0F00) >> 8); } fn get_inst_kk(inst: u16) u8 { return @truncate(inst & 0x00FF); } fn get_inst_y(inst: u16) u4 { return @truncate((inst & 0x00F0) >> 4); } fn get_inst_nnn(inst: u16) u12 { return @truncate(inst & 0x0FFF); } fn get_inst_n(inst: u16) u4 { return @truncate(inst & 0x000F); } pub fn init(allocator: Allocator) !Self { const seed_bits: u128 = @bitCast(std.time.nanoTimestamp()); const seed: u64 = @truncate(seed_bits); const display_width = 64; const display_height = 32; const memory_size = 4096; const memory = try allocator.alloc(u8, memory_size); @memset(memory, 0); var self = Self { .allocator = allocator, .display = try allocator.alloc(bool, display_width * display_height), .display_width = display_width, .display_height = display_height, .memory = memory, .stack = [1]u16{0} ** 16, .V = [1]u8{0} ** 16, .PC = 0x200, .rng = std.rand.DefaultPrng.init(seed), .input = [1]bool{false} ** 16 }; self.init_default_sprites(); return self; } pub fn deinit(self: *Self) void { self.allocator.free(self.display); self.allocator.free(self.memory); } pub fn init_default_sprites(self: *Self) void { self.set_memory(0x000, &[_]u8{ 0xF0, 0x90, 0x90, 0x90, 0xF0, // "0" 0x20, 0x60, 0x20, 0x20, 0x70, // "1" 0xF0, 0x10, 0xF0, 0x80, 0xF0, // "2" 0xF0, 0x10, 0xF0, 0x10, 0xF0, // "3" 0x90, 0x90, 0xF0, 0x10, 0x10, // "4" 0xF0, 0x80, 0xF0, 0x10, 0xF0, // "5" 0xF0, 0x80, 0xF0, 0x90, 0xF0, // "6" 0xF0, 0x10, 0x20, 0x40, 0x40, // "7" 0xF0, 0x90, 0xF0, 0x90, 0xF0, // "8" 0xF0, 0x90, 0xF0, 0x10, 0xF0, // "9" 0xF0, 0x90, 0xF0, 0x90, 0x90, // "A" 0xE0, 0x90, 0xE0, 0x90, 0xE0, // "B" 0xF0, 0x80, 0x80, 0x80, 0xF0, // "C" 0xE0, 0x90, 0x90, 0x90, 0xE0, // "D" 0xF0, 0x80, 0xF0, 0x80, 0xF0, // "E" 0xF0, 0x80, 0xF0, 0x80, 0x80 // "F" }); } pub fn display_get(self: *const Self, x: u8, y: u8) bool { const idx: u16 = @as(u16, y) * self.display_width + x; return self.display[idx]; } pub fn display_set(self: *Self, x: u8, y: u8, value: bool) void { const idx: u16 = @as(u16, y) * self.display_width + x; self.display[idx] = value; } pub fn memory_set(self: *Self, address: u16, value: u8) !void { if (address >= self.memory.len) { return Errors.MemoryOutOfBounds; } self.memory[address] = value; } pub fn memory_get(self: *const Self, address: u16) !u8 { if (address >= self.memory.len) { return Errors.MemoryOutOfBounds; } return self.memory[address]; } pub fn draw(self: *Self, x: u8, y: u8, n: u4) bool { var result = false; for (0..n) |i| { const sprite_row = self.memory[self.I + i]; for (0..8) |ix| { const disp_x: u8 = @intCast((x+ix) % self.display_width); const disp_y: u8 = @intCast((y+i) % self.display_height); const sprite_pixel = ((sprite_row >> @intCast(7-ix)) & 1) == 1; const display_pixel = self.display_get(disp_x, disp_y); const new_pixel = (display_pixel != sprite_pixel); self.display_set(disp_x, disp_y, new_pixel); if (display_pixel and display_pixel != new_pixel) { result = true; } } } return result; } pub fn is_input_pressed(self: *const Self, key: u4) bool { return self.input[key]; } pub fn set_memory(self: *Self, base_address: u16, memory: []const u8) void { for (0.., memory) |offset, byte| { self.memory[base_address + offset] = byte; } } pub fn set_memory_from_file(self: *Self, base_address: u16, file: std.fs.File) !void { const stat = try file.stat(); var buffer: [512]u8 = undefined; var offset: usize = 0; var bytes_left: i64 = @intCast(stat.size); while (bytes_left > 0) : (bytes_left -= buffer.len) { const bytes_read = try file.read(&buffer); for (0..bytes_read) |byte_index| { self.memory[base_address + offset + byte_index] = buffer[byte_index]; } offset += buffer.len; } } pub fn current_instruction(self: *const Self) u16 { const high_byte: u16 = self.memory[self.PC]; const low_byte: u16 = self.memory[self.PC+1]; return (high_byte << 8) | low_byte; } pub fn step(self: *Self) !void { if (self.PC >= self.memory.len) { return Errors.ProgramCounterOutOfBounds; } const instruction: u16 = self.current_instruction(); self.PC += 2; try self.run_instruction(instruction); } pub fn update_timer(self: *Self) void { if (self.DT > 0) { self.DT -= 1; } if (self.ST > 0) { self.ST -= 1; } } pub fn clear_display(self: *Self) void { const display_size = @as(u16, self.display_width) * self.display_height; for (0..display_size) |i| { self.display[i] = false; } } pub fn decode_instruction_fmt(inst: u16) [*:0]const u8 { if (inst & 0xFFFF == 0x00E0) { // 00E0 - CLS return "CLS"; } else if (inst & 0xFFFF == 0x00EE) { // 00EE - RET return "RET"; } else if (inst & 0xF000 == 0x0000) { // 0nnn - SYS addr return "SYS {nnn}"; } else if (inst & 0xF000 == 0x1000) { // 1nnn - JP addr return "JP {nnn}"; } else if (inst & 0xF000 == 0x2000) { // 2nnn - CALL addr return "CALL {nnn}"; } else if (inst & 0xF000 == 0x3000) { // 3xkk - SE Vx, byte return "SE V{x}, {kk}"; } else if (inst & 0xF000 == 0x4000) { // 4xkk - SNE Vx, byte return "SNE V{x}, {kk}"; } else if (inst & 0xF00F == 0x5000) { // 5xy0 - SE Vx, Vy return "SE V{x}, V{y}"; } else if (inst & 0xF000 == 0x6000) { // 6xkk - LD Vx, byte return "LD V{x}, {kk}"; } else if (inst & 0xF000 == 0x7000) { // 7xkk - ADD Vx, byte return "ADD V{x}, {kk}"; } else if (inst & 0xF000 == 0x8000) { if (inst & 0x000F == 0x0) { // 8xy0 - LD Vx, Vy return "LD V{x}, V{y}"; } else if (inst & 0x000F == 0x1) { // 8xy1 - OR Vx, Vy return "OR V{x}, V{y}"; } else if (inst & 0x000F == 0x2) { // 8xy2 - AND Vx, Vy return "AND V{x}, V{y}"; } else if (inst & 0x000F == 0x3) { // 8xy3 - XOR Vx, Vy return "XOR V{x}, V{y}"; } else if (inst & 0x000F == 0x4) { // 8xy4 - ADD Vx, Vy, set VF = carry return "ADD V{x}, V{y}"; } else if (inst & 0x000F == 0x5) { // 8xy5 - SUB Vx, Vy, set VF = NOT borrow return "SUB V{x}, V{y}"; } else if (inst & 0x000F == 0x6) { // 8xy6 - Vx SHR 1 return "SHR V{x}"; } else if (inst & 0x000F == 0x7) { // 8xy7 - SUBN Vx, Vy return "SUBN V{x}, V{y}"; } else if (inst & 0x000F == 0xE) { // 8xyE - Vx SHR 1 return "SHL V{x}"; } } else if (inst & 0xF00F == 0x9000) { // 9xy0 - SNE Vx, Vy return "SNE V{x}, V{y}"; } else if (inst & 0xF000 == 0xA000) { // Annn - LD I, addr return "LD I, {nnn}"; } else if (inst & 0xF000 == 0xB000) { // Bnnn - JP V0, addr return "JP V0, {nnn}"; } else if (inst & 0xF000 == 0xC000) { // Cxkk - RNG Vx, byte return "RNG V{x}, {kk}"; } else if (inst & 0xF000 == 0xD000) { // Dxyn - DRW Vx, Vy, nibble return "DRW V{x}, V{x}, {n}\n"; } else if (inst & 0xF0FF == 0xE09E) { // Ex9E - SKP Vx return "SKP V{x}"; } else if (inst & 0xF0FF == 0xE0A1) { // ExA1 - SKNP Vx return "SKNP V{x}"; } else if (inst & 0xF000 == 0xF000) { if (inst & 0x00FF == 0x07) { // Fx07 - LD Vx, DT return "LD V{x}, DT"; } else if (inst & 0x00FF == 0x0A) { // Fx0A - LD Vx, K return "LD V{x}, K"; } else if (inst & 0x00FF == 0x15) { // Fx15 - LD DT, Vx return "LD DT, V{x}"; } else if (inst & 0x00FF == 0x18) { // Fx1E - LD ST, Vx return "LD ST, V{x}"; } else if (inst & 0x00FF == 0x1E) { // Fx1E - ADD I, Vx return "ADD I, V{x}"; } else if (inst & 0x00FF == 0x29) { // Fx29 - LD F, Vx return "LD F, V{x}"; } else if (inst & 0x00FF == 0x33) { // Fx33 - LD B, Vx return "LD B, V{x}"; } else if (inst & 0x00FF == 0x55) { // Fx55 - LD [I], Vx return "LD [I], V{x}"; } else if (inst & 0x00FF == 0x65) { // Fx65 - LD Vx, [I] return "LD V{x}, [I]"; } } return ""; } pub fn run_instruction(self: *Self, inst: u16) !void { if (inst & 0xFFFF == 0x00E0) { // 00E0 - CLS self.clear_display(); } else if (inst & 0xFFFF == 0x00EE) { // 00EE - RET if (self.SP == 0) return Errors.StackOverflow; self.PC = self.stack[self.SP]; self.SP -= 1; } else if (inst & 0xF000 == 0x0000) { // 0nnn - SYS addr // Ignore } else if (inst & 0xF000 == 0x1000) { // 1nnn - JP addr const addr = inst & 0x0FFF; self.PC = addr; } else if (inst & 0xF000 == 0x2000) { // 2nnn - CALL addr if (self.SP >= self.stack.len) return Errors.StackOverflow; const addr = inst & 0x0FFF; self.SP += 1; self.stack[self.SP] = self.PC; self.PC = addr; } else if (inst & 0xF000 == 0x3000) { // 3xkk - SE Vx, byte const Vx = get_inst_x(inst); const byte = get_inst_kk(inst); if (self.V[Vx] == byte) { self.PC += 2; } } else if (inst & 0xF000 == 0x4000) { // 4xkk - SNE Vx, byte const x = get_inst_x(inst); const byte = get_inst_kk(inst); if (self.V[x] != byte) { self.PC += 2; } } else if (inst & 0xF00F == 0x5000) { // 5xy0 - SE Vx, Vy const Vx = get_inst_x(inst); const Vy = get_inst_y(inst); if (self.V[Vx] == self.V[Vy]) { self.PC += 2; } } else if (inst & 0xF000 == 0x6000) { // 6xkk - LD Vx, byte const Vx = get_inst_x(inst); const byte = get_inst_kk(inst); self.V[Vx] = byte; } else if (inst & 0xF000 == 0x7000) { // 7xkk - ADD Vx, byte const x = get_inst_x(inst); const byte = get_inst_kk(inst); self.V[x] +%= byte; } else if (inst & 0xF000 == 0x8000) { if (inst & 0x000F == 0x0) { // 8xy0 - LD Vx, Vy const x = get_inst_x(inst); const y = get_inst_y(inst); self.V[x] = self.V[y]; } else if (inst & 0x000F == 0x1) { // 8xy1 - OR Vx, Vy const x = get_inst_x(inst); const y = get_inst_y(inst); self.V[x] |= self.V[y]; } else if (inst & 0x000F == 0x2) { // 8xy2 - AND Vx, Vy const x = get_inst_x(inst); const y = get_inst_y(inst); self.V[x] &= self.V[y]; } else if (inst & 0x000F == 0x3) { // 8xy3 - XOR Vx, Vy const Vx = get_inst_x(inst); const Vy = get_inst_y(inst); self.V[Vx] ^= self.V[Vy]; } else if (inst & 0x000F == 0x4) { // 8xy4 - ADD Vx, Vy, set VF = carry const x = get_inst_x(inst); const y = get_inst_y(inst); const result = @addWithOverflow(self.V[x], self.V[y]); self.V[x] = result[0]; self.V[0xF] = (1 - result[1]); } else if (inst & 0x000F == 0x5) { // 8xy5 - SUB Vx, Vy, set VF = NOT borrow const x = get_inst_x(inst); const y = get_inst_y(inst); const result = @subWithOverflow(self.V[x], self.V[y]); self.V[x] = result[0]; self.V[0xF] = (1 - result[1]); } else if (inst & 0x000F == 0x6) { // 8xy6 - Vx SHR 1 const x = get_inst_x(inst); self.V[0xF] = self.V[x] & 1; self.V[x] = (self.V[x] >> 1); } else if (inst & 0x000F == 0x7) { // 8xy7 - SUBN Vx, Vy const x = get_inst_x(inst); const y = get_inst_y(inst); const result = @subWithOverflow(self.V[y], self.V[x]); self.V[x] = result[0]; self.V[0xF] = (1 - result[1]); } else if (inst & 0x000F == 0xE) { // 8xyE - Vx SHR 1 const Vx = get_inst_x(inst); self.V[0xF] = self.V[Vx] & 128; self.V[Vx] = (self.V[Vx] << 1); } else { return Errors.UnknownInstruction; } } else if (inst & 0xF00F == 0x9000) { // 9xy0 - SNE Vx, Vy const Vx = get_inst_x(inst); const Vy = get_inst_y(inst); if (self.V[Vx] != self.V[Vy]) { self.PC += 2; } } else if (inst & 0xF000 == 0xA000) { // Annn - LD I, addr const addr = get_inst_nnn(inst); self.I = addr; } else if (inst & 0xF000 == 0xB000) { // Bnnn - JP V0, addr const addr = get_inst_nnn(inst); self.PC = self.V[0] + addr; } else if (inst & 0xF000 == 0xC000) { // Cxkk - RNG Vx, byte const Vx = get_inst_x(inst); const mask = get_inst_kk(inst); self.V[Vx] = self.rng.random().int(u8) & mask; } else if (inst & 0xF000 == 0xD000) { // Dxyn - DRW Vx, Vy, nibble const Vx = get_inst_x(inst); const Vy = get_inst_y(inst); const n = get_inst_n(inst); self.V[0xF] = @intFromBool(self.draw(self.V[Vx], self.V[Vy], n)); } else if (inst & 0xF0FF == 0xE09E) { // Ex9E - SKP Vx const x = get_inst_x(inst); if (self.is_input_pressed(@truncate(self.V[x]))) { self.PC += 2; } } else if (inst & 0xF0FF == 0xE0A1) { // ExA1 - SKNP Vx const x = get_inst_x(inst); if (!self.is_input_pressed(@truncate(self.V[x]))) { self.PC += 2; } } else if (inst & 0xF000 == 0xF000) { if (inst & 0x00FF == 0x07) { // Fx07 - LD Vx, DT const x = get_inst_x(inst); self.V[x] = self.DT; } else if (inst & 0x00FF == 0x0A) { // Fx0A - LD Vx, K const x = get_inst_x(inst); if (!self.is_input_pressed(@truncate(self.V[x]))) { self.PC -= 2; } } else if (inst & 0x00FF == 0x15) { // Fx15 - LD DT, Vx const x = get_inst_x(inst); self.DT = self.V[x]; } else if (inst & 0x00FF == 0x18) { // Fx1E - LD ST, Vx const x = get_inst_x(inst); self.ST = self.V[x]; } else if (inst & 0x00FF == 0x1E) { // Fx1E - ADD I, Vx const x = get_inst_x(inst); self.I += self.V[x]; } else if (inst & 0x00FF == 0x29) { // Fx29 - LD F, Vx const x = get_inst_x(inst); self.I = self.V[x] * 5; } else if (inst & 0x00FF == 0x33) { // Fx33 - LD B, Vx const x = get_inst_x(inst); const Vx = self.V[x]; try self.memory_set(self.I+0, @divFloor(Vx, 100)); try self.memory_set(self.I+1, @divFloor(Vx, 10) % 10); try self.memory_set(self.I+2, Vx % 10); } else if (inst & 0x00FF == 0x55) { // Fx55 - LD [I], Vx const x = get_inst_x(inst); var i: u4 = 0; while (i <= x) : (i += 1) { try self.memory_set(self.I + i, self.V[i]); } } else if (inst & 0x00FF == 0x65) { // Fx65 - LD Vx, [I] const x = get_inst_x(inst); var i: u4 = 0; while (i <= x) : (i += 1) { self.V[i] = try self.memory_get(self.I + i); } } else { return Errors.UnknownInstruction; } } else { return Errors.UnknownInstruction; } }