From ffbb823d6e7a0e92a59dedce45c2346dd3651875 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 27 Aug 2023 14:46:52 +0300 Subject: [PATCH] initial commit --- .gitignore | 2 + README.md | 3 + ROMs/flightrunner.ch8 | Bin 0 -> 295 bytes ROMs/morse_demo.ch8 | Bin 0 -> 371 bytes ROMs/octojam1title.ch8 | Bin 0 -> 426 bytes ROMs/test_opcode.ch8 | Bin 0 -> 478 bytes build.zig | 37 ++++ build.zig.zon | 10 ++ raylib-zig | 1 + src/chip.zig | 379 +++++++++++++++++++++++++++++++++++++++++ src/main.zig | 147 ++++++++++++++++ 11 files changed, 579 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ROMs/flightrunner.ch8 create mode 100644 ROMs/morse_demo.ch8 create mode 100644 ROMs/octojam1title.ch8 create mode 100644 ROMs/test_opcode.ch8 create mode 100644 build.zig create mode 100644 build.zig.zon create mode 160000 raylib-zig create mode 100644 src/chip.zig create mode 100755 src/main.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c82b07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-cache +zig-out diff --git a/README.md b/README.md new file mode 100644 index 0000000..d835e55 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# CHIP-8 Emulator + +Techinal reference: http://devernay.free.fr/hacks/chip8/C8TECH10.HTM diff --git a/ROMs/flightrunner.ch8 b/ROMs/flightrunner.ch8 new file mode 100644 index 0000000000000000000000000000000000000000..b379353df5f48e63828b7676faa6ac4125b15cb6 GIT binary patch literal 295 zcmWfZ{P02I!+)1mt5&(3U3s*nH^(8HIZLKU~Co?!( zC0hnRXjBNf(5Mio!0--aLZ3me1IV03ObINB3_YxOHa32_vk-_U0&!#G7e{8L`#uag z4mJ)9?+!30GJ@RWz|bw?z}O|~zyze3yF{B7G;K9vWq8-Hp(0uj;)}c&EAxQ(KoZ0j0_g+7T|m|?Wr?JU zPokgLofw3c2+dKJ5E4t~PZIeo0TtUJw1MGWGHVhST;!C{A%=HJTu}KNLKlEMIVkU$ z&^?BCi|sCm0!e2u=>aBv!DJAa3vY&8Qv{!s`;PC!0?WN zV!+7E%+AhU_y7O%!_Ca>3=9om05LWp0b%@uf`Wtx2M~M+3#gTukwL+LoqztnKR*~4 zY-q&gWG4>-7M32|0T^Hfn$QgUHn-{Ih_ zq9GYDMQo}6r>QE8%&eS7$1E5anb>%&zx=ZR8fsRb9m>F<@$Lq5bu9yfNV%h9vLnQ? aK$7DC{{zMc;xl9~a2$|$z;S>PDgyxVYI6Jl literal 0 HcmV?d00001 diff --git a/ROMs/test_opcode.ch8 b/ROMs/test_opcode.ch8 new file mode 100644 index 0000000000000000000000000000000000000000..f540f69ecfc06e6c7b30881234469cc10847bff9 GIT binary patch literal 478 zcmYk&ze~eF6bJC8;uTt`rKMFO)T4t#DiW)7aF9c=aT1sA@kf%j0Y~YWsiDx-J-lv3 z1R;Mw{sLz$9o|@rh&s6H-K7zijmckgkn@idt{U=U!$7$S`jO^N|hYBI{u zLvEBJ7rsFj@J-|)dXX2fAb|8VOudV>H0vnc)n3+BEX}S%S^y zPBf8sb5KpBZAvM?b|SYwtvFSN-Nf$VlnRYlYR8@R?q-j=3S_x7MK{?=O%i)k6%A5AWWClvaIsC#&di4 cz5iuFT33MVtpI6c> 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); +} + +const ChipErrors = error { UnknownInstruction }; + +pub const ChipContext = struct { + pub const MEMORY_SIZE: u16 = 4096; + pub const DISPLAY_WIDTH: u16 = 64; + pub const DISPLAY_HEIGHT: u16 = 32; + pub const DISPLAY_SIZE = DISPLAY_WIDTH * DISPLAY_HEIGHT; + + display: [DISPLAY_SIZE]bool, + memory: [MEMORY_SIZE]u8, + stack: [16]u16, + rng: std.rand.DefaultPrng, + input: [16]bool, + + V: [16]u8, + + I: u16, // Address pointer + PC: u16, // Program counter + SP: u8, // Stack pointer + DT: u8, // Delay timer + ST: u8, // Sound timer + + pub fn init() ChipContext { + const seed_bits: u128 = @bitCast(std.time.nanoTimestamp()); + const seed: u64 = @truncate(seed_bits); + + return ChipContext{ + .display = [1]bool{false} ** DISPLAY_SIZE, + .memory = [1]u8{0} ** MEMORY_SIZE, + .stack = [1]u16{0} ** 16, + .V = [1]u8{0} ** 16, + .I = 0, + .PC = 0x200, + .SP = 0, + .DT = 0, + .ST = 0, + .rng = std.rand.DefaultPrng.init(seed), + .input = [1]bool{false} ** 16 + }; + } + + pub fn display_get(self: *ChipContext, x: u8, y: u8) bool { + return self.display[y * ChipContext.DISPLAY_WIDTH + x]; + } + + pub fn display_set(self: *ChipContext, x: u8, y: u8, value: bool) void { + self.display[y * ChipContext.DISPLAY_WIDTH + x] = value; + } + + pub fn draw(self: *ChipContext, 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) % ChipContext.DISPLAY_WIDTH); + const disp_y: u8 = @intCast((y+i) % ChipContext.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: *ChipContext, key: u4) bool { + return self.input[key]; + } + + pub fn set_memory(self: *ChipContext, 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: *ChipContext, 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 step(self: *ChipContext) !void { + const high_byte: u16 = self.memory[self.PC]; + const low_byte: u16 = self.memory[self.PC+1]; + const instruction: u16 = (high_byte << 8) | low_byte; + self.PC += 2; + try self.run_instruction(instruction); + } + + pub fn update_timer(self: *ChipContext) void { + if (self.DT > 0) { + self.DT -= 1; + } + if (self.ST > 0) { + self.ST -= 1; + } + } + + pub fn clear_display(self: *ChipContext) void { + for (0..DISPLAY_SIZE) |i| { + self.display[i] = false; + } + } + + pub fn run_instruction(self: *ChipContext, inst: u16) !void { + print("[{x:0>4}] ({x:0>4}) ", .{ self.PC, inst }); + + if (inst & 0xFFFF == 0x00E0) { // 00E0 - CLS + print("CLS\n", .{}); + self.clear_display(); + + } else if (inst & 0xFFFF == 0x00EE) { // 00EE - RET + self.PC = self.stack[self.SP]; + self.SP -= 1; + print("RET\n", .{}); + + } else if (inst & 0xF000 == 0x0000) { // 0nnn - SYS addr + const addr = inst & 0x0FFF; + print("SYS 0x{x}\n", .{ addr }); + + } else if (inst & 0xF000 == 0x1000) { // 1nnn - JP addr + const addr = inst & 0x0FFF; + self.PC = addr; + print("JP 0x{x}\n", .{ addr }); + + } else if (inst & 0xF000 == 0x2000) { // 2nnn - CALL addr + const addr = inst & 0x0FFF; + self.SP += 1; + self.stack[self.SP] = self.PC; + self.PC = addr; + print("CALL 0x{x}\n", .{ 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; + } + print("SE V{x}, 0x{x}\n", .{ Vx, byte }); + + } 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; + } + print("SNE V{x}, 0x{x}\n", .{ x, byte }); + + } 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; + } + print("SE V{x}, V{x}\n", .{ Vx, Vy }); + + } 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; + print("LD V{x}, 0x{x}\n", .{ 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; + print("ADD V{x}, 0x{x}\n", .{ 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]; + print("LD V{x}, V{x}\n", .{ x, 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]; + print("OR V{x}, V{x}\n", .{ x, 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]; + print("AND V{x}, V{x}\n", .{ x, 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]; + print("XOR V{x}, V{x}\n", .{ Vx, 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]); + print("ADD V{x}, V{x}\n", .{ x, y }); + + } 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]); + print("SUB V{x}, V{x}\n", .{ x, y }); + + } 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); + print("SHR V{x}\n", .{ x }); + + } else if (inst & 0x000F == 0x7) { // 8xy7 - SUBN Vx, Vy + const Vx = get_inst_x(inst); + const Vy = get_inst_y(inst); + const sub: i16 = self.V[Vy] - self.V[Vx]; + const not_borrow = @intFromBool(sub > 0); + self.V[Vx] = @intCast(sub & 0x00FF); + self.V[0xF] = not_borrow; + print("SUBN V{x}, V{x}\n", .{ Vx, Vy }); + + } 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); + print("SHL V{x}\n", .{ Vx }); + + } else { + print("UNKNOWN\n", .{ }); + return ChipErrors.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; + } + print("SNE V{x}, V{x}\n", .{ Vx, Vy }); + + } else if (inst & 0xF000 == 0xA000) { // Annn - LD I, addr + const addr = get_inst_nnn(inst); + self.I = addr; + print("LD I, 0x{x}\n", .{ addr }); + + } else if (inst & 0xF000 == 0xB000) { // Bnnn - JP V0, addr + const addr = get_inst_nnn(inst); + self.PC = self.V[0] + addr; + print("JP V0, 0x{x}\n", .{ 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; + print("RNG V{x}, 0x{x}\n", .{ Vx, 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)); + print("DRW V{x}, V{x}, 0x{x}\n", .{ Vx, 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; + } + print("SKP V{x}\n", .{ x }); + + } 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; + } + print("SKNP V{x}\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF007) { // Fx07 - LD Vx, DT + const x = get_inst_x(inst); + self.V[x] = self.DT; + print("LD V{x}, DT\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF00A) { // Fx0A - LD Vx, K + const x = get_inst_x(inst); + if (!self.is_input_pressed(@truncate(self.V[x]))) { + self.PC -= 2; + } + print("LD V{x}, K\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF015) { // Fx15 - LD DT, Vx + const x = get_inst_x(inst); + self.DT = self.V[x]; + print("LD DT, V{x}\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF018) { // Fx1E - LD ST, Vx + const x = get_inst_x(inst); + self.ST = self.V[x]; + print("LD ST, V{x}\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF01E) { // Fx1E - ADD I, Vx + const x = get_inst_x(inst); + self.I += self.V[x]; + print("ADD I, V{x}\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF029) { // Fx29 - LD F, Vx + const x = get_inst_x(inst); + self.I = self.V[x] * 5; + print("LD F, V{x}\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF033) { // Fx33 - LD B, Vx + const x = get_inst_x(inst); + const Vx = self.V[x]; + self.memory[self.I+0] = @divFloor(Vx, 100); + self.memory[self.I+1] = @divFloor(Vx, 10) % 10; + self.memory[self.I+2] = Vx % 10; + print("LD B, V{x}\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF055) { // Fx55 - LD [I], Vx + const x = get_inst_x(inst); + var i: u4 = 0; + while (i <= x) : (i += 1) { + self.memory[self.I + i] = self.V[i]; + } + print("LD [I], V{x}\n", .{ x }); + + } else if (inst & 0xF0FF == 0xF065) { // Fx65 - LD Vx, [I] + const x = get_inst_x(inst); + var i: u4 = 0; + while (i <= x) : (i += 1) { + self.V[i] = self.memory[self.I + i]; + } + print("LD V{x}, [I]\n", .{ x }); + + } else { + print("UNKNOWN\n", .{ }); + return ChipErrors.UnknownInstruction; + } + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100755 index 0000000..06ffd5f --- /dev/null +++ b/src/main.zig @@ -0,0 +1,147 @@ +const rl = @import("raylib"); +const std = @import("std"); +const ChipContext = @import("chip.zig").ChipContext; + +pub fn render_display(chip: *ChipContext, x: i32, y: i32, pixel_size: i32, on_color: rl.Color, off_color: rl.Color) void { + for (0..ChipContext.DISPLAY_HEIGHT) |oy| { + for (0..ChipContext.DISPLAY_WIDTH) |ox| { + const ix = x + @as(i32, @intCast(ox)) * pixel_size; + const iy = y + @as(i32, @intCast(oy)) * pixel_size; + const pixel = chip.display[oy * ChipContext.DISPLAY_WIDTH + ox]; + const color = if (pixel) on_color else off_color; + rl.drawRectangle(ix, iy, pixel_size, pixel_size, color); + } + } +} + +pub fn read_keyboard_input(chip: *ChipContext) void { + const keys = [16]rl.KeyboardKey{ + .key_x, + .key_one, + .key_two, + .key_three, + .key_q, + .key_w, + .key_e, + .key_a, + .key_s, + .key_d, + .key_z, + .key_c, + .key_four, + .key_r, + .key_f, + .key_v, + }; + for (0.., keys) |i, key| { + chip.input[i] = rl.isKeyDown(key); + } +} + +pub fn create_sin_wave(samples: []i16, sample_rate: u32, duration: u32, frequency: u32, volume: f32) void { + const sample_rate_float: f32 = @floatFromInt(sample_rate); + const frequency_float: f32 = @floatFromInt(frequency); + + const sample_size = 16; + const max_sample_value: f32 = @floatFromInt((1 << (sample_size-1)) - 1); + + const frame_count = sample_rate * duration; + for (1..frame_count) |i| { + const i_float: f32 = @floatFromInt(i); + const wave: f32 = @sin(std.math.pi*2*frequency_float/sample_rate_float*i_float); + samples[i] = @intFromFloat(wave*max_sample_value * volume); + } +} + +pub fn main() anyerror!void { + var chip = ChipContext.init(); + + chip.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" + }); + + { + const file = try std.fs.cwd().openFile("morse_demo.ch8", .{ .mode = .read_only }); + defer file.close(); + try chip.set_memory_from_file(0x200, file); + } + + + const pixel_size = 20; + const screen_width = ChipContext.DISPLAY_WIDTH * pixel_size; + const screen_height = ChipContext.DISPLAY_HEIGHT * pixel_size; + + rl.initWindow(screen_width, screen_height, "CHIP-8"); + defer rl.closeWindow(); + + rl.initAudioDevice(); + defer rl.closeAudioDevice(); + + rl.setTargetFPS(60); + + var chip_speed: f32 = 500; + var timer_speed: f32 = 60; + + const sample_rate = 44100; + var data = [1]i16{0} ** sample_rate; + create_sin_wave(&data, sample_rate, 1.0, 440, 0.2); + var chip_wave = rl.Wave{ + .frameCount = sample_rate, + .sampleRate = sample_rate, + .sampleSize = 16, + .channels = 1, + .data = @ptrCast(&data), + }; + var chip_sound = rl.loadSoundFromWave(chip_wave); + defer rl.unloadSound(chip_sound); + + var chip_clock_time: f32 = 0; + var chip_timer_timer: f32 = 0; + while (!rl.windowShouldClose()) { + var dt = rl.getFrameTime(); + chip_clock_time += dt; + chip_timer_timer += dt; + read_keyboard_input(&chip); + + while (chip_clock_time > 1/chip_speed) { + chip.step() catch {}; + chip_clock_time -= 1/chip_speed; + } + + while (chip_timer_timer > 1/timer_speed) { + chip.update_timer(); + chip_timer_timer -= 1/timer_speed; + } + + if (chip.ST > 0) { + if (!rl.isSoundPlaying(chip_sound)) { + rl.playSound(chip_sound); + } + } else { + rl.stopSound(chip_sound); + } + + { + rl.beginDrawing(); + defer rl.endDrawing(); + + rl.clearBackground(rl.Color.white); + render_display(&chip, 0, 0, pixel_size, rl.Color.black, rl.Color.ray_white); + } + } +}