#!/usr/bin/env python3 import random import sys import pyglet # TODO: Don't hardcode keyboard input keypad_keys = [ pyglet.window.key._1, pyglet.window.key._2, pyglet.window.key._3, pyglet.window.key._4, pyglet.window.key.Q, pyglet.window.key.W, pyglet.window.key.E, pyglet.window.key.R, pyglet.window.key.A, pyglet.window.key.S, pyglet.window.key.D, pyglet.window.key.F, pyglet.window.key.Z, pyglet.window.key.X, pyglet.window.key.C, pyglet.window.key.V, ] keypad_keys = list(zip(keypad_keys, [ pyglet.window.key.NUM_7, pyglet.window.key.NUM_8, pyglet.window.key.NUM_9, pyglet.window.key.NUM_SUBTRACT, pyglet.window.key.NUM_4, pyglet.window.key.NUM_5, pyglet.window.key.NUM_6, pyglet.window.key.NUM_ADD, pyglet.window.key.NUM_1, pyglet.window.key.NUM_2, pyglet.window.key.NUM_3, pyglet.window.key.NUM_ENTER, pyglet.window.key.NUM_DIVIDE, pyglet.window.key.NUM_0, pyglet.window.key.NUM_MULTIPLY, pyglet.window.key.NUM_SEPARATOR, ])) # OSCOM Nano keys are arranged like # 123C # 456D # 789E # A0BF # according to http://hobbylabs.org/oscom_nano.htm # However, at the moment the indices of the keys are # 0123 # 4567 # 89ab # cdef # This rearranges them so that the index and the hex digit match keypad_keys = [keypad_keys[i] for i in [0xd, 0, 1, 2, 4, 5, 6, 8, 9, 0xa, 0xc, 0xe, 3, 7, 0xb, 0xf]] def key_pressed(symbol): global keys_pressed, keypress_arrived for i in range(16): if symbol in keypad_keys[i]: keys_pressed[i] = True keypress_arrived = True break def key_released(symbol): global keys_pressed for i in range(16): if symbol in keypad_keys[i]: keys_pressed[i] = False break def draw_screen(): global display_screen to_draw = [] for y in range(32): for x in range(64): if display_screen[y * 64 + x]: to_draw.append((x, y)) screen_points = [] for x, y in to_draw: # pyglet has 0,0 at bottom left, so reverse the y y = 31 - y # TODO: Don't hardcode pixel size # Lol, what is a winding order screen_points.append(x * 10) screen_points.append(y * 10) screen_points.append(x * 10 + 10) screen_points.append(y * 10) screen_points.append(x * 10 + 10) screen_points.append(y * 10 + 10) screen_points.append(x * 10) screen_points.append(y * 10 + 10) if len(screen_points) > 0: pyglet.graphics.draw(len(screen_points) // 2, pyglet.gl.GL_QUADS, ('v2i', screen_points) ) def step(): global data_registers, ip, stack, i_register global ram, font_start global screen global delay_timer, sound_timer global keys_pressed, waiting_for_keypress, keypress_arrived # Keep track of whether we did a draw or a jump, for the draw # sprint feature did_draw = False did_jump = False # Don't execute any code in a waitstate, as it'd only end up # busylooping if waiting_for_keypress and not keypress_arrived: return did_draw, did_jump high_byte = ram[ip] ip = (ip + 1) & 0xfff low_byte = ram[ip] ip = (ip + 1) & 0xfff #print(hex(high_byte), hex(low_byte), hex(ip), [hex(i) for i in data_registers], [hex(i) for i in stack], hex(i_register), delay_timer)#debg #input()#debg # Instruction info gotten from http://mattmik.com/files/chip8/mastering/chip8.html # 00E0 clearscreen if high_byte == 0x00 and low_byte == 0xE0: screen = [False] * 64 * 32 # 00EE ret elif high_byte == 0x00 and low_byte == 0xEE: ip = stack.pop() did_jump = True # 0nnn call_machine elif high_byte >> 4 == 0: # Our own non-standard debug if high_byte & 0xf == 0x1: print(low_byte & 0xf, data_registers[low_byte & 0xf]) else: print("%03x: Can't call machine language!" % (ip - 2)) sys.exit(1) # 1nnn jmp nnn elif high_byte >> 4 == 1: ip = ((high_byte & 0xf) << 8) | low_byte did_jump = True # 2nnn call nnn elif high_byte >> 4 == 2: stack.append(ip) ip = ((high_byte & 0xf) << 8) | low_byte did_jump = True # 3xnn skipeq vx, nn elif high_byte >> 4 == 3: if data_registers[high_byte & 0xf] == low_byte: ip = (ip + 2) & 0xfff # 4xnn skipneq vx, nn elif high_byte >> 4 == 4: if data_registers[high_byte & 0xf] != low_byte: ip = (ip + 2) & 0xfff # 5xy0 skipeq vx, vy elif high_byte >> 4 == 5 and low_byte & 0xf == 0: if data_registers[high_byte & 0xf] == data_registers[low_byte >> 4]: ip = (ip + 2) & 0xfff # 6xnn mov vx, nn elif high_byte >> 4 == 6: data_registers[high_byte & 0xf] = low_byte # 7xnn add vx, nn (no flags) elif high_byte >> 4 == 7: old_value = data_registers[high_byte & 0xf] data_registers[high_byte & 0xf] = (old_value + low_byte) & 0xff # 8xy0 mov vx, vy elif high_byte >> 4 == 8 and low_byte & 0xf == 0: data_registers[high_byte & 0xf] = data_registers[low_byte >> 4] # 8xy1 or vx, vy elif high_byte >> 4 == 8 and low_byte & 0xf == 1: old_value = data_registers[high_byte & 0xf] result = old_value | data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result # 8xy2 and vx, vy elif high_byte >> 4 == 8 and low_byte & 0xf == 2: old_value = data_registers[high_byte & 0xf] result = old_value & data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result # 8xy3 xor vx, vy elif high_byte >> 4 == 8 and low_byte & 0xf == 3: old_value = data_registers[high_byte & 0xf] result = old_value ^ data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result # 8xy4 add vx, vy (carry flag) elif high_byte >> 4 == 8 and low_byte & 0xf == 4: old_value = data_registers[high_byte & 0xf] result = old_value + data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result & 0xff if result > 255: data_registers[0xf] = 1 else: data_registers[0xf] = 0 # 8xy5 sub vx, vy (carry flag) elif high_byte >> 4 == 8 and low_byte & 0xf == 5: old_value = data_registers[high_byte & 0xf] result = old_value - data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result & 0xff if result < 0: data_registers[0xf] = 0 else: data_registers[0xf] = 1 # 8xy6 shr vx, vy, 1 (LSB to VF) elif high_byte >> 4 == 8 and low_byte & 0xf == 6: other_value = data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = other_value >> 1 data_registers[0xf] = other_value & 0x1 # 8xy7 sub vx, vy, vx (carry flag) elif high_byte >> 4 == 8 and low_byte & 0xf == 7: old_value = data_registers[high_byte & 0xf] result = data_registers[low_byte >> 4] - old_value data_registers[high_byte & 0xf] = result & 0xff if result < 0: data_registers[0xf] = 0 else: data_registers[0xf] = 1 # 8xyE shl vx, vy, 1 (MSB to VF) elif high_byte >> 4 == 8 and low_byte & 0xf == 0xE: other_value = data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = (other_value << 1) & 0xff data_registers[0xf] = other_value >> 7 # 9xy0 skipneq vx, vy elif high_byte >> 4 == 9 and low_byte & 0xf == 0: if data_registers[high_byte & 0xf] != data_registers[low_byte >> 4]: ip = (ip + 2) & 0xfff # Annn mov i, nnn elif high_byte >> 4 == 0xA: i_register = ((high_byte & 0xf) << 8) | low_byte # Bnnn jmp nnn + v0 elif high_byte >> 4 == 0xB: ip = ((high_byte & 0xf) << 8) | low_byte ip = (ip + data_registers[0]) & 0xfff did_jump = True # Cxnn maskedrandom vx, nn elif high_byte >> 4 == 0xC: result = random.randint(0, 255) data_registers[high_byte & 0xf] = result & low_byte # Dxyn draw vx, vy, n elif high_byte >> 4 == 0xD: # OSCOM Nano manual (page 38) says "", but I can't find any info on how to do this # wrapping on the generally used sources. # # https://github.com/AfBu/haxe-chip-8-emulator looks like # it wraps the pixels to the next row down, but as it # doesn't seem to handle drawing out the bottom of the # screen at all, unsure how intentional this is. # # https://github.com/dmatlack/chip8 wraps the pixels on the # other side of the same line. # # The game "Blitz" seems to require draws off the bottom of # the screen to not result in anything. Therefore, as I # have yet to find any games that break with it, I'm just # skipping pixels that fall outside the screen. did_draw = True x_start = data_registers[high_byte & 0xf] y_start = data_registers[low_byte >> 4] any_unset = False # Sprite will be 8 pixels wide and n tall for dy in range(low_byte & 0xf): # Load this line's graphics line = ram[(i_register + dy) & 0xfff] y = y_start + dy # Screen is 32 lines tall if y >= 32: continue for dx in range(8): x = x_start + dx # Screen is 64 columns wide if x >= 64: continue # Pixels are stored MSB-left pixel = (line >> (7 - dx)) & 1 screen_index = y * 64 + x if pixel: # Check if we are unsetting a pixel if screen[screen_index]: any_unset = True # XOR the pixel screen[screen_index] = not screen[screen_index] if any_unset: data_registers[0xf] = 1 else: data_registers[0xf] = 0 # Ex9E skipkey vx elif high_byte >> 4 == 0xE and low_byte == 0x9E: if keys_pressed[data_registers[high_byte & 0xf]]: ip = (ip + 2) & 0xfff # ExA1 skipnkey vx elif high_byte >> 4 == 0xE and low_byte == 0xA1: if not keys_pressed[data_registers[high_byte & 0xf]]: ip = (ip + 2) & 0xfff # Fx07 getdelay vx elif high_byte >> 4 == 0xF and low_byte == 0x07: data_registers[high_byte & 0xf] = delay_timer # Fx0A getkey vx elif high_byte >> 4 == 0xF and low_byte == 0x0A: if not waiting_for_keypress: # Wait for a key to be pressed and then re-execute # this instruction waiting_for_keypress = True keypress_arrived = False ip = (ip - 2) & 0xfff if keypress_arrived: # A key was pressed for i in range(16): if keys_pressed[i]: # Set vx to first key we found any_pressed = True data_registers[high_byte & 0xf] = i break waiting_for_keypress = False # Fx15 setdelay vx elif high_byte >> 4 == 0xF and low_byte == 0x15: delay_timer = data_registers[high_byte & 0xf] # Fx18 setsound vx elif high_byte >> 4 == 0xF and low_byte == 0x18: sound_timer = data_registers[high_byte & 0xf] # Fx1E add i, vx elif high_byte >> 4 == 0xF and low_byte == 0x1E: i_register = (i_register + data_registers[high_byte & 0xf]) & 0xfff # Fx29 mov i, digit(vx) elif high_byte >> 4 == 0xF and low_byte == 0x29: value = data_registers[high_byte & 0xf] if 0 <= value <= 0xf: # Each digit is 5 lines tall = 5 bytes long i_register = font_start + value * 5 else: print('%03x: Bad font character' % (ip-2)) sys.exit(1) # Fx33 mov [i … i+2], bcd(vx) elif high_byte >> 4 == 0xF and low_byte == 0x33: value = data_registers[high_byte & 0xf] highdigit = value // 100 middigit = value // 10 % 10 lowdigit = value % 10 ram[i_register] = highdigit ram[(i_register + 1) & 0xfff] = middigit ram[(i_register + 2) & 0xfff] = lowdigit # Fx55 mov [i … i+x], v0 … vx (updates i) elif high_byte >> 4 == 0xF and low_byte == 0x55: for register in range((high_byte & 0xf) + 1): ram[i_register] = data_registers[register] i_register = (i_register + 1) & 0xfff # Fx65 mov v0 … vx, [i … i+x] (updates i) elif high_byte >> 4 == 0xF and low_byte == 0x65: for register in range((high_byte & 0xf) + 1): data_registers[register] = ram[i_register] i_register = (i_register + 1) & 0xfff # Fallback else: print('%03x: Unrecognized!' % (ip-2)) return did_draw, did_jump def tick_timers(): global delay_timer, sound_timer # Tick the delay timer down until it reaches 0 if delay_timer > 0: # The timer should tick down at 60Hz delay_timer -= 1 # Play a sound using pyglet if we have a non-zero sound timer # It will run asynchronously from the other parts, which means it # won't experience slowdown similarily to the delay timer # Unsure if this is good or bad if sound_timer > 0: # sound_timer is in the unit of 1/60th of a second, while # Square takes length in seconds pyglet.media.synthesis.Square(sound_timer/60).play() sound_timer = 0 def advance_interpreter(dt): global cpu_clock global screen, display_screen, screen_fps global cpu_cycles_to_go, frames_to_go # Each tic (60Hz) we need to run cpu_clock / 60Hz cycles and # draw screen_fps / 60 frames cpu_cycles_to_go += cpu_clock / 60 frames_to_go += screen_fps / 60 tick_timers() while cpu_cycles_to_go >= 1: did_draw, _ = step() cpu_cycles_to_go -= 1 # If we did a draw, run upto two ticks' cycles to reach # the next draw. Chip-8 draws are often paired, an erase # and a draw. If we don't hit both in the same frame, it # will cause flicker. # Since this will put the number of cycles to go below zero # if we run out of our own cycles, on average we will run # the right number of cycles per tick # Also, to avoid pairing draw half of one pair and erase # half of another, the sprint is invalidated by a jump, a # call or a ret if did_draw: #sprint_run_out = True#debg for cycle in range(2*cpu_clock // 60): did_draw, did_jump = step() cpu_cycles_to_go -= 1 # Exit as soon as we hit the other draw if did_draw: #sprint_run_out = False#debg #print('Sprint succeeded! %i' % cycle)#debg break if did_jump: #sprint_run_out = False#debg #print('Sprint failed! (jump) %i' % cycle)#debg break #if sprint_run_out: print('Sprint failed! (ran out)')#debg if frames_to_go >= 1: # Only draw the latest frame. This means FPS >60 won't work # but why would you ever want to run chip-8 faster than # that display_screen = screen[:] frames_to_go -= int(frames_to_go) def initialize_ram(): global ram, font_start ram = [0]*(1<<12) # Font data is from http://mattmik.com/files/chip8/mastering/chip8.html # and http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#2.4 font = [ 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 ] # Unsure where the font should go. Both http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/ # and http://stevelosh.com/blog/2016/12/chip8-graphics/?m=1#fonts # say it goes at 0x50, but most resources don't have its location # specified and I have run into implementations that have it at # start of memory (http://craigthomas.ca/blog/2017/10/15/writing-a-chip-8-emulator-built-in-font-set-part-4/) # and even a resource stating it's at high memory (http://www.multigesture.net/wp-content/uploads/mirror/goldroad/chip8.shtml) # The location honestly shouldn't matter, but I'm putting it at # 0x50 font_start = 0x50 ram[font_start:font_start + len(font)] = font test_program = [ ## Arithmetic test #0x65, 0x42, # 0x42 → v5 | v5: 42 #0x82, 0x50, # v5 → v2 | v2: 42, v5: 42 #0x72, 0x69, # v2 + 0x69 → v2 | v2: ab, v5: 42 #0x85, 0x24, # v5 + v2 → v5 | v2: ab, v5: ed, vf: 00 #0x85, 0x24, # v5 + v2 → v5 | v2: ab, v5: 98, vf: 01 #0x85, 0x25, # v5 - v2 → v5 | v2: ab, v5: ed, vf: 00 #0x82, 0x57, # v5 - v2 → v2 | v2: 42, v5: ed, vf: 01 #0x63, 0x01, # 0x01 → v3 | v2: 42, v3: 01, v5: ed, vf: 01 #0x85, 0x22, # v5 & v2 → v5 | v2: 42, v3: 01, v5: 40, vf: 01 #0x83, 0x51, # v3 | v5 → v3 | v2: 42, v3: 41, v5: 40, vf: 01 #0x82, 0x33, # v2 ^ v3 → v2 | v2: 3, v3: 41, v5: 40, vf: 01 #0x82, 0x56, # v5 >> 1 → v2 | v2: 20, v3: 41, v5: 40, vf: 00 #0x85, 0x36, # v3 >> 1 → v5 | v2: 20, v3: 41, v5: 20, vf: 01 #0x64, 0x70, # 0x70 → v4 | v2: 20, v3: 41, v4: 70, v5: 20, vf: 01 #0x84, 0x4E, # v4 << 1 → v4 | v2: 20, v3: 41, v4: e0, v5: 20, vf: 00 #0x84, 0x4E, # v4 << 1 → v4 | v2: 20, v3: 41, v4: c0, v5: 20, vf: 01 ## RNG test #0xC0, 0xff, # RNG → v0 #0xC0, 0xff, # RNG → v0 #0xC0, 0xff, # RNG → v0 #0xC0, 0xff, # RNG → v0 #0xC0, 0x06, # RNG & 0x00001110 → v0 #0xC0, 0x06, # RNG & 0x00001110 → v0 #0xC0, 0x06, # RNG & 0x00001110 → v0 #0xC0, 0x06, # RNG & 0x00001110 → v0 ## Flow control test #0x60, 0x00, # mov v0, 0 #0x22, 0x06, # call 0x206 #0x12, 0x02, # jmp 0x202 #0x70, 0x01, # add v0, 1 #0x00, 0xee, # ret ## Skip test #0x61, 0xf0, # mov v1, 0xf0 #0x62, 0xf1, # mov v2, 0xf1 #0x83, 0x10, # mov v3, v1 #0x31, 0xf0, # skipeq v3, 0xf0 #0x00, 0x00, # ill #0x31, 0x00, # skipeq v3, 0 #0x6e, 0x01, # mov ve, 1 #0x51, 0x30, # skipeq v1, v3 #0x00, 0x00, # ill #0x42, 0xf0, # skipneq v2, 0xf0 #0x00, 0x00, # ill #0x91, 0x20, # skipneq v1, v2 #0x00, 0x00, # ill #0x6d, 0x01, # mov vd, 1 ## Timer test #0xF0, 0x07, # getdelay v0 #0x40, 0x00, # skipneq v0, 0 #0x22, 0x08, # call 0x208 #0x12, 0x00, # jmp 0x200 #0x60, 0xff, # mov v0, 0xff #0xf0, 0x15, # setdelay v0 #0x00, 0xee, # ret ## Input wait test #0xf1, 0x0a, # getkey v1 ## Input skip test #0x61, 0x00, # mov v1, 0 #0x62, 0x0a, # mov v2, 0xa #0x63, 0x0b, # mov v3, 0xb #0xe2, 0xa1, # skipnkey v2 #0x71, 0x01, # add v1, 1 #0xe3, 0x9e, # skipkey v3 #0x12, 0x06, # jmp 0x206 ## Input wait test the second #0xa1, 0x23, # mov i, 0x123 #0xf0, 0x0a, # getkey v0 #0xf0, 0x1e, # add i, v0 #0x12, 0x02, # jmp 0x202 ## Drawing program / drawing test ## From the OSCOM Nano manual #0x6a, 0x01, # mov va, 1 #0x60, 0x10, # mov v0, 0x10 #0x61, 0x20, # mov v1, 0x20 #0xa2, 0x50, # mov i, 0x250 #0xf2, 0x0a, # getkey v2 #0x42, 0x01, # skipneq v2, 1 #0x22, 0x2e, # call 0x22e #0x42, 0x02, # skipneq v2, 2 #0x80, 0xa5, # sub v0, va #0x42, 0x03, # skipneq v2, 3 #0x22, 0x34, # call 0x234 #0x42, 0x04, # skipneq v2, 4 #0x81, 0xa5, # sub v1, va #0x42, 0x06, # skipneq v2, 6 #0x81, 0xa4, # add v1, va #0x42, 0x07, # skipneq v2, 7 #0x22, 0x3a, # call 0x23a #0x42, 0x08, # skipneq v2, 8 #0x80, 0xa4, # add v0, va #0x42, 0x09, # skipneq v2, 9 #0x22, 0x40, # call 0x240 #0xD1, 0x01, # draw v0, v1, 1 #0x12, 0x08, # jmp 0x208 ## 22e #0x81, 0xa5, # sub v1, va #0x80, 0xa5, # sub v0, va #0x00, 0xee, # ret ## 234 #0x80, 0xa5, # sub v0, va #0x81, 0xa4, # add v1, va #0x00, 0xee, # ret ## 23a #0x80, 0xa4, # add v0, va #0x81, 0xa5, # sub v1, va #0x00, 0xee, # ret ## 240 #0x80, 0xa4, # add v0, va #0x81, 0xa4, # add v1, va #0x00, 0xee, # ret ## This was missing from the original ## Actually, never mind, it was just separately ## 246 #0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ## 250 #0x80, ## Font test #0x60, 0x0a, # mov v0, 0x0a #0xf1, 0x0a, # getkey v1 #0x00, 0xe0, # clearscreen #0xf1, 0x29, # mov i, digit(v1) #0xd0, 0x05, # draw v0, v0, 5 #0x12, 0x02, # jmp 0x202 ## Base converter ## From the OSCOM Nano manual ## Modified to run several times and display input #0x6a, 0x00, # mov va, 0 #0x6b, 0x00, # mov vb, 0 #0xf0, 0x0a, # getkey v0 #0x82, 0x00, # mov v2, v0 #0x00, 0xe0, # clearscreen #0xf0, 0x29, # mov i, digit(v0) #0xda, 0xb5, # draw va, vb, 5 #0x7a, 0x06, # add va, 6 #0xf1, 0x0a, # getkey v1 #0xf1, 0x29, # mov i, digit(v1) #0xda, 0xb5, # draw va, vb, 5 #0x7b, 0x07, # add vb, 7 #0x6a, 0x00, # mov va, 0 #0x63, 0x00, # mov v3, 0 ## 21c #0x80, 0x24, # add v0, v2 #0x73, 0x01, # add v3, 1 #0x33, 0x0f, # skipeq v3, 0xf #0x12, 0x1c, # jmp 0x21c #0x80, 0x14, # add v0, v1 #0xA2, 0x50, # mov i, 0x250 #0xf0, 0x33, # mov [i … i+2], bcd(v0) #0xf2, 0x65, # mov v0 … v2, [i … i+2] #0xf0, 0x29, # mov i, digit(v0) #0xda, 0xb5, # draw va, vb, 5 #0x7a, 0x06, # add va, 6 #0xf1, 0x29, # mov i, digit(v1) #0xda, 0xb5, # draw va, vb, 5 #0x7a, 0x06, # add va, 6 #0xf2, 0x29, # mov i, digit(v2) #0xda, 0xb5, # draw va, vb, 5 #0x12, 0x00, # jmp 0x200 ] #ram[0x200:len(test_program) + 0x200] = test_program def initialize_screen(): global screen, display_screen, screen_fps screen = [False] * 64 * 32 display_screen = screen[:] # TODO: Support changing FPS screen_fps = 60 def initialize_timers(): global delay_timer, sound_timer delay_timer = 0 sound_timer = 0 def initialize_keyboard(): global keys_pressed, waiting_for_keypress, keypress_arrived keys_pressed = [False]*16 waiting_for_keypress = False keypress_arrived = False def initialize_cpu(clockspeed): global cpu_clock, data_registers, ip, stack, i_register data_registers = [0] * 16 # According to the OSCOM Nano manual the program is loaded here ip = 0x200 # It doesn't seem to be specified where this is stored, so put it # in its own "address space" stack = [] i_register = 0 cpu_clock = clockspeed def load_program(f): global ram # ram is an array of numbers, not a bytestring program = [i for i in f.read()] # Load at 0x200 ram[0x200: 0x200 + len(program)] = program def main(): global window global cpu_cycles_to_go, frames_to_go if len(sys.argv) == 2: program_path = sys.argv[1] # Run the interpreter at 500Hz by default # This was picked from https://github.com/AfBu/haxe-CHIP-8-emulator/wiki/(Super)CHIP-8-Secrets cpu_clock = 500 elif len(sys.argv) == 3: program_path = sys.argv[1] cpu_clock = int(sys.argv[2]) else: print(f'Usage: {sys.argv[0]} program [cpu-speed]', file=sys.stderr) sys.exit(1) # TODO: Don't hardcode the size window = pyglet.window.Window(640, 320, resizable = True) # Don't use pulse driver, as it is really buggy and can crash the # python process pyglet.options['audio'] = ('openal', 'directsound', 'silent') # Hook up our screen drawing routine @window.event def on_draw(): # Clear the screen window.clear() # Draw things draw_screen() # Handle keyboard input @window.event def on_key_press(symbol, modifiers): key_pressed(symbol) @window.event def on_key_release(symbol, modifiers): key_released(symbol) initialize_ram() initialize_screen() initialize_timers() initialize_keyboard() initialize_cpu(cpu_clock) # TODO: Deal with missing file gracefully with open(program_path, 'rb') as f: load_program(f) cpu_cycles_to_go = 0 frames_to_go = 0 # Start the emulation pyglet.clock.schedule_interval(advance_interpreter, 1/60) pyglet.app.run() if __name__ == '__main__': main()