use fmt; use fs; use io; use math::random; use os; use time; use types; use sdl2; def SCALE: int = 20; def CLOCKSPEED: int = 600; def VOLUME: i16 = 3000; def SCREEN_WIDTH: size = 64; def SCREEN_HEIGHT: size = 32; def MEM_SIZE: size = 4096; def START_ADDR: u16 = 0x200; def FONT_SIZE: size = 80; def FONT: [FONT_SIZE]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 ]; type inst = struct { op: u16, X: u8, Y: u8, N: u8, NN: u8, NNN: u16 }; type state = struct { mem: [MEM_SIZE]u8, display: [SCREEN_WIDTH * SCREEN_HEIGHT]bool, draw: bool, current_key_pressed: u8, keys: [16]bool, stack: [16]u16, SP: u16, PC: u16, V: [16]u8, I: u16, DT: u8, ST: u8, }; fn chip8() state = { let state = state { PC = START_ADDR, current_key_pressed = 0xFF, ... }; state.mem[..FONT_SIZE] = FONT[..]; return state; }; fn instruction(opcode: u16) inst = inst { op = opcode, X = (opcode >> 8): u8 & 0x0F, Y = (opcode >> 4): u8 & 0x0F, N = opcode: u8 & 0x0F, NN = opcode: u8 & 0xFF, NNN = opcode & 0x0FFF, }; fn print_instruction(state: *state, inst: inst) void = { fmt::printf("Address: 0x{:.4X}, OP: 0x{:.4X}, Desc: ", state.PC - 2, inst.op)!; switch ((inst.op & 0xF000) >> 12) { case 0x00 => if (inst.NNN == 0x0E0) { fmt::printfln("Clear screen")!; } else if (inst.NNN == 0x0EE) { fmt::printfln("Return from subroutine to 0x{:.4X}", state.stack[state.SP - 1])!; } else fmt::printfln("Unkown OP code")!; case 0x01 => fmt::printfln("Jump to 0x{:.4X}", inst.NNN)!; case 0x02 => fmt::printfln("Call subroutine at 0x{:.4X}", inst.NNN)!; case 0x03 => fmt::printfln("Skip the next instruction if V{:X} (0x{:.2X}) == 0x{:.2X}", inst.X, state.V[inst.X], inst.NN)!; case 0x04 => fmt::printfln("Skip the next instruction if V{:X} (0x{:.2X}) != 0x{:.2X}", inst.X, state.V[inst.X], inst.NN)!; case 0x05 => fmt::printfln("Skip the next instruction if V{:X} (0x{:.2X}) == V{:X} (0x{:.2X})", inst.X, state.V[inst.X], inst.Y, state.V[inst.Y])!; case 0x06 => fmt::printfln("V{:X} (0x{:.2X}) = 0x{:.2X}", inst.X, state.V[inst.X], inst.NN)!; case 0x07 => fmt::printfln("V{:X} (0x{:.2X}) += 0x{:.2X}", inst.X, state.V[inst.X], inst.NN)!; case 0x08 => switch (inst.N) { case 0x00 => fmt::printfln("V{:X} (0x{:.2X}) = V{:X} (0x{:.2X})", inst.X, state.V[inst.X], inst.Y, state.V[inst.Y])!; case 0x01 => fmt::printfln("V{:X} (0x{:.2X}) |= V{:X} (0x{:.2X})", inst.X, state.V[inst.X], inst.Y, state.V[inst.Y])!; case 0x02 => fmt::printfln("V{:X} (0x{:.2X}) &= V{:X} (0x{:.2X})", inst.X, state.V[inst.X], inst.Y, state.V[inst.Y])!; case 0x03 => fmt::printfln("V{:X} (0x{:.2X}) ^= V{:X} (0x{:.2X})", inst.X, state.V[inst.X], inst.Y, state.V[inst.Y])!; case 0x04 => fmt::printfln("V{:X} (0x{:.2X}) += V{:X} (0x{:.2X})", inst.X, state.V[inst.X], inst.Y, state.V[inst.Y])!; case 0x05 => fmt::printfln("V{:X} (0x{:.2X}) -= V{:X} (0x{:.2X})", inst.X, state.V[inst.X], inst.Y, state.V[inst.Y])!; case 0x06 => fmt::printfln("V{:X} = V{:X} (0x{:.2X}) >> 1", inst.X, inst.Y, state.V[inst.Y])!; case 0x07 => fmt::printfln("V{:X} = V{:X} (0x{:.2X}) - V{:X} (0x{:.2X})", inst.X, inst.Y, state.V[inst.Y], inst.X, state.V[inst.X])!; case 0x0E => fmt::printfln("V{:X} = V{:X} (0x{:.2X}) << 1", inst.X, inst.Y, state.V[inst.Y])!; case => fmt::printfln("Unkown OP code")!; }; case 0x09 => fmt::printfln("Skip the next instruction if V{:X} (0x{:.2X}) != V{:X} (0x{:.2X})", inst.X, state.V[inst.X], inst.Y, state.V[inst.Y])!; case 0x0A => fmt::printfln("I (0x{:.4X}) = 0x{:.4X}", state.I, inst.NNN)!; case 0x0B => fmt::printfln("Jump to 0x{:.4X}", state.V[0x00] + inst.NNN)!; case 0x0C => fmt::printfln("V{:X} (0x{:.2X}) = rand() & 0x{:.2X}", inst.X, state.V[inst.X], inst.NN)!; case 0x0D => fmt::printfln("Draw 8 pixel wide, {} pixel tall sprite at X = V{:X} and Y = V{:X} ({}, {}), reading from memory location 0x{:.4X} onward", inst.N, inst.X, inst.Y, state.V[inst.X], state.V[inst.Y], state.I)!; case 0x0E => if (inst.NN == 0x9E) { fmt::printfln("Skip next instruction if the key stored in V{:X} (0x{:X}) is pressed", inst.X, state.V[inst.X] & 0x0F)!; } else if (inst.NN == 0xA1) { fmt::printfln("Skip next instruction if the key stored in V{:X} (0x{:X}) is not pressed", inst.X, state.V[inst.X] & 0x0F)!; } else fmt::printfln("Unkown OP code")!; case 0x0F => switch (inst.NN) { case 0x07 => fmt::printfln("V{:X} (0x{:.2X}) = delay timer (0x{:.2X})", inst.X, state.V[inst.X], state.DT)!; case 0x0A => fmt::printfln("Wait for keypress and store it in V{:X} (0x{:.2X})", inst.X, state.V[inst.X])!; case 0x15 => fmt::printfln("Set delay timer (0x{:.2X}) to V{:X} (0x{:.2X})", state.DT, inst.X, state.V[inst.X])!; case 0x18 => fmt::printfln("Set sound timer (0x{:.2X}) to V{:X} (0x{:.2X})", state.ST, inst.X, state.V[inst.X])!; case 0x1E => fmt::printfln("I += V{:X} (0x{:.2X})", inst.X, state.V[inst.X])!; case 0x29 => fmt::printfln("I = sprite location for character {:X} (0x{:.4X})", state.V[inst.X] & 0x0F, (state.V[inst.X] & 0x0F) * 5)!; case 0x33 => fmt::printfln("Stores the BCD representation of V{:X} (0x{:.2X}) with hundreds at 0x{:.4X}, tens at 0x{:.4X} and ones at 0x{:.4X}", inst.X, state.V[inst.X], state.I, state.I + 1, state.I + 2)!; case 0x55 => fmt::printfln("Stores from V0 to (and including) V{:X} in memory, starting at 0x{:.4X}", inst.X, state.I)!; case 0x65 => fmt::printfln("Fills from V0 to (and including) V{:X} with values from memory, starting at 0x{:.4X}", inst.X, state.I)!; case => fmt::printfln("Unkown OP code")!; }; case => fmt::printfln("Unkown OP code")!; }; }; fn execute_instruction(state: *state, inst: inst, random: *random::random) void = { switch ((inst.op & 0xF000) >> 12) { case 0x00 => if (inst.NNN == 0x0E0) { state.display[..] = [false...]; state.draw = true; } else if (inst.NNN == 0x0EE) { state.SP -= 1; state.PC = state.stack[state.SP]; }; case 0x01 => state.PC = inst.NNN; case 0x02 => state.stack[state.SP] = state.PC; state.SP += 1; state.PC = inst.NNN; case 0x03 => if (state.V[inst.X] == inst.NN) state.PC += 2; case 0x04 => if (state.V[inst.X] != inst.NN) state.PC += 2; case 0x05 => if (state.V[inst.X] == state.V[inst.Y]) state.PC += 2; case 0x06 => state.V[inst.X] = inst.NN; case 0x07 => state.V[inst.X] += inst.NN; case 0x08 => switch (inst.N) { case 0x00 => state.V[inst.X] = state.V[inst.Y]; case 0x01 => state.V[0x0F] = 0; state.V[inst.X] |= state.V[inst.Y]; case 0x02 => state.V[0x0F] = 0; state.V[inst.X] &= state.V[inst.Y]; case 0x03 => state.V[0x0F] = 0; state.V[inst.X] ^= state.V[inst.Y]; case 0x04 => state.V[0x0F] = if (state.V[inst.X] > types::U8_MAX - state.V[inst.Y]) 1 else 0; state.V[inst.X] += state.V[inst.Y]; case 0x05 => state.V[0x0F] = if (state.V[inst.X] >= state.V[inst.Y]) 1 else 0; state.V[inst.X] -= state.V[inst.Y]; case 0x06 => state.V[0x0F] = state.V[inst.Y] & 0x01; state.V[inst.X] = state.V[inst.Y] >> 1; case 0x07 => state.V[0x0F] = if (state.V[inst.Y] >= state.V[inst.X]) 1 else 0; state.V[inst.X] = state.V[inst.Y] - state.V[inst.X]; case 0x0E => state.V[0x0F] = (state.V[inst.Y] & 0x80) >> 7; state.V[inst.X] = state.V[inst.Y] << 1; case => return; }; case 0x09 => if (state.V[inst.X] != state.V[inst.Y]) state.PC += 2; case 0x0A => state.I = inst.NNN; case 0x0B => state.PC = state.V[0x00] + inst.NNN; case 0x0C => state.V[inst.X] = (random::next(random) & 255): u8 & inst.NN; case 0x0D => const rx = state.V[inst.X]; const ry = state.V[inst.Y]; const sprite_height = inst.N; let sprite_row = 0u8; state.V[0x0F] = 0; for (let y = 0u8; y < sprite_height; y += 1) { sprite_row = state.mem[state.I + y]; for (let x = 0u8; x < 8; x += 1) { if ((sprite_row & (0x80 >> x)) != 0) { let pixel = &state.display[((ry + y) * SCREEN_WIDTH + rx + x) % (SCREEN_WIDTH * SCREEN_HEIGHT)]; if (*pixel) state.V[0x0F] = 1; *pixel = !*pixel; }; }; }; state.draw = true; case 0x0E => if (inst.NN == 0x9E) { if (state.keys[state.V[inst.X] & 0x0F]) state.PC += 2; } else if (inst.NN == 0xA1) { if (!state.keys[state.V[inst.X] & 0x0F]) state.PC += 2; }; case 0x0F => switch (inst.NN) { case 0x07 => state.V[inst.X] = state.DT; case 0x0A => for (let i = 0z; state.current_key_pressed == 0xFF && i < len(state.keys); i += 1) if (state.keys[i]) { state.current_key_pressed = i: u8; break; }; if (state.current_key_pressed == 0xFF) state.PC -= 2 else { if (state.keys[state.current_key_pressed]) state.PC -= 2 else { state.V[inst.X] = state.current_key_pressed; state.current_key_pressed = 0xFF; }; }; case 0x15 => state.DT = state.V[inst.X]; case 0x18 => state.ST = state.V[inst.X]; case 0x1E => state.I += state.V[inst.X]; case 0x29 => state.I = (state.V[inst.X] & 0x0F) * 5; case 0x33 => let BCD = state.V[inst.X]; state.mem[state.I + 2] = BCD % 10; BCD /= 10; state.mem[state.I + 1] = BCD % 10; BCD /= 10; state.mem[state.I] = BCD; case 0x55 => for (let i = 0u8; i <= inst.X; i += 1) { state.mem[state.I] = state.V[i]; state.I += 1; }; case 0x65 => for (let i = 0u8; i <= inst.X; i += 1) { state.V[i] = state.mem[state.I]; state.I += 1; }; case => return; }; case => return; }; }; fn tick_timers(state: *state, audio_device: SDL_AudioDeviceID) void = { if (state.DT > 0) state.DT -= 1; if (state.ST > 0) { state.ST -= 1; SDL_PauseAudioDevice(audio_device, 0); } else { SDL_PauseAudioDevice(audio_device, 1); }; }; fn tick(state: *state, random: *random::random, debug: bool) void = { let op_hi = state.mem[state.PC]: u16; let op_lo = state.mem[state.PC + 1]: u16; let op = (op_hi << 8) | op_lo; state.PC += 2; if (debug) print_instruction(state, instruction(op)); execute_instruction(state, instruction(op), random); }; fn update_screen(state: *state, renderer: *sdl2::SDL_Renderer) (void | sdl2::error) = { let rect = sdl2::SDL_Rect { x = 0, y = 0, w = SCALE, h = SCALE }; for (let i = 0z; i < len(state.display); i += 1) { rect.x = (i % SCREEN_WIDTH): int * SCALE; rect.y = (i / SCREEN_WIDTH): int * SCALE; if (state.display[i]) { sdl2::SDL_SetRenderDrawColor(renderer, 0xFE, 0xFE, 0xFE, 0xFF)?; sdl2::SDL_RenderFillRect(renderer, &rect)?; sdl2::SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF)?; sdl2::SDL_RenderDrawRect(renderer, &rect)?; } else { sdl2::SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF)?; sdl2::SDL_RenderFillRect(renderer, &rect)?; }; }; sdl2::SDL_RenderPresent(renderer); }; fn audio_callback(userdata: nullable *opaque, stream: *u8, len_: int) void = { let stream: []i16 = *(&types::slice { data = stream: *[*]i16, length = len_: size / 2, capacity = 0 }: *[]i16); let sample_index = 0; const half_square_wave_period = 44100 / 440 / 2; for (let i = 0; i < len_ / 2; i += 1) { stream[i] = if ((sample_index / half_square_wave_period) % 2 == 0) -VOLUME else VOLUME; sample_index += 1; }; }; fn run() (void | fs::error | io::error | sdl2::error) = { if (len(os::args) < 2) { fmt::fatalf("Usage: {} ", os::args[0]); }; let debug = os::getenv("CHIP8_DEBUG") is str; const chip8 = chip8(); let seed = time::now(time::clock::REALTIME).nsec: u64; let random = random::init(seed); const rom = os::open(os::args[1])?; defer io::close(rom)!; io::read(rom, chip8.mem[START_ADDR..])?; sdl2::SDL_Init(sdl2::SDL_INIT_VIDEO | sdl2::SDL_INIT_AUDIO)?; defer sdl2::SDL_Quit(); let window = sdl2::SDL_CreateWindow("CHIP-8", sdl2::SDL_WINDOWPOS_CENTERED, sdl2::SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH: int * SCALE, SCREEN_HEIGHT: int * SCALE, sdl2::SDL_WindowFlags::SHOWN)?; defer sdl2::SDL_DestroyWindow(window); let renderer = sdl2::SDL_CreateRenderer(window, -1, sdl2::SDL_RendererFlags::ACCELERATED)?; defer sdl2::SDL_DestroyRenderer(renderer); const spec = SDL_AudioSpec { freq = 44100, format = AUDIO_S16LSB, channels = 1, samples = 512, callback = &audio_callback, userdata = null, ... }; let dev = SDL_OpenAudioDevice(null, 0, &spec, null, 0)?; defer SDL_CloseAudioDevice(dev); sdl2::SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF)?; sdl2::SDL_RenderClear(renderer)?; let quit = false; for (!quit) { let event = sdl2::event { ... }; for (sdl2::SDL_PollEvent(&event)? == 1) switch (event.event_type) { case sdl2::SDL_EventType::QUIT => quit = true; case sdl2::SDL_EventType::KEYDOWN => switch (event.key.keysym.scancode) { case sdl2::SDL_Scancode::ESCAPE => quit = true; case sdl2::SDL_Scancode::TWO => chip8.keys[0x1] = true; case sdl2::SDL_Scancode::THREE => chip8.keys[0x2] = true; case sdl2::SDL_Scancode::FOUR => chip8.keys[0x3] = true; case sdl2::SDL_Scancode::FIVE => chip8.keys[0xC] = true; case sdl2::SDL_Scancode::W => chip8.keys[0x4] = true; case sdl2::SDL_Scancode::E => chip8.keys[0x5] = true; case sdl2::SDL_Scancode::R => chip8.keys[0x6] = true; case sdl2::SDL_Scancode::T => chip8.keys[0xD] = true; case sdl2::SDL_Scancode::S => chip8.keys[0x7] = true; case sdl2::SDL_Scancode::D => chip8.keys[0x8] = true; case sdl2::SDL_Scancode::F => chip8.keys[0x9] = true; case sdl2::SDL_Scancode::G => chip8.keys[0xE] = true; case sdl2::SDL_Scancode::X => chip8.keys[0xA] = true; case sdl2::SDL_Scancode::C => chip8.keys[0x0] = true; case sdl2::SDL_Scancode::V => chip8.keys[0xB] = true; case sdl2::SDL_Scancode::B => chip8.keys[0xF] = true; case => void; }; case sdl2::SDL_EventType::KEYUP => switch (event.key.keysym.scancode) { case sdl2::SDL_Scancode::ESCAPE => quit = true; case sdl2::SDL_Scancode::TWO => chip8.keys[0x1] = false; case sdl2::SDL_Scancode::THREE => chip8.keys[0x2] = false; case sdl2::SDL_Scancode::FOUR => chip8.keys[0x3] = false; case sdl2::SDL_Scancode::FIVE => chip8.keys[0xC] = false; case sdl2::SDL_Scancode::W => chip8.keys[0x4] = false; case sdl2::SDL_Scancode::E => chip8.keys[0x5] = false; case sdl2::SDL_Scancode::R => chip8.keys[0x6] = false; case sdl2::SDL_Scancode::T => chip8.keys[0xD] = false; case sdl2::SDL_Scancode::S => chip8.keys[0x7] = false; case sdl2::SDL_Scancode::D => chip8.keys[0x8] = false; case sdl2::SDL_Scancode::F => chip8.keys[0x9] = false; case sdl2::SDL_Scancode::G => chip8.keys[0xE] = false; case sdl2::SDL_Scancode::X => chip8.keys[0xA] = false; case sdl2::SDL_Scancode::C => chip8.keys[0x0] = false; case sdl2::SDL_Scancode::V => chip8.keys[0xB] = false; case sdl2::SDL_Scancode::B => chip8.keys[0xF] = false; case => void; }; case => void; }; const frame_start = sdl2::SDL_GetPerformanceCounter(); for (let i = 0; i < CLOCKSPEED / 60; i += 1) { tick(&chip8, &random, debug); if ((chip8.mem[chip8.PC - 2] >> 4) == 0x0D) break; }; const frame_end = sdl2::SDL_GetPerformanceCounter(); const time_elapsed = ((frame_end - frame_start) * 1000): f64 / sdl2::SDL_GetPerformanceFrequency(): f64; sdl2::SDL_Delay(if (16.67 > time_elapsed) (16.67 - time_elapsed): u32 else 0); if (chip8.draw) { update_screen(&chip8, renderer)?; chip8.draw = false; }; tick_timers(&chip8, dev); }; }; export fn main() void = { match (run()) { case let err: fs::error => fmt::fatal("FS error: ", fs::strerror(err)); case let err: io::error => fmt::fatal("I/O error: ", io::strerror(err)); case let err: sdl2::error => fmt::fatal("SDL error: ", sdl2::strerror(err)); case void => return; }; };