package chip8 import ( _ "embed" "encoding/binary" "image" "math" ) //go:embed beep.wav var Beep []byte const ( MemorySize = 4096 // 4KB of memory InstructionSize = 2 // 2 bytes long instructions ) const ( ProgramAddress = uint16(0x0200) ) const ( Width = 64 Height = 32 FPS = 60 CyclesPerFrame = 8 SampleRate = 44100 ) type KeyPressed func(key uint8) bool type SoundPlayer func(sound []byte) (func(), func()) type ROM struct { Name string Data []byte } type Emulator struct { V [16]uint8 // general registers I uint16 // address register PC uint16 // program counter DT uint8 // delay timer ST uint8 // sound timer Stack *Stack // very simple stack Memory [MemorySize]uint8 // 4KB of system RAM ROM ROM // game rom Display *image.RGBA // display buffer KeyPressed KeyPressed // input function PlaySound func() // play sound effect StopSound func() // stop sound effect } func NewEmulator(keyPressed KeyPressed, soundPlayer SoundPlayer) *Emulator { emulator := new(Emulator) emulator.KeyPressed = keyPressed emulator.PlaySound, emulator.StopSound = soundPlayer(Beep) emulator.Stack = NewStack() emulator.Display = image.NewRGBA(image.Rect(0, 0, Width, Height)) emulator.Reset() return emulator } func (emulator *Emulator) Reset() { emulator.V = [16]uint8{} emulator.I = 0 emulator.PC = ProgramAddress emulator.DT = 0 emulator.ST = 0 emulator.Memory = [MemorySize]uint8{} emulator.Stack.Clear() emulator.ClearScreen() emulator.LoadROM(emulator.ROM) emulator.LoadFont() } func (emulator *Emulator) LoadROM(rom ROM) { emulator.ROM = rom copy(emulator.Memory[ProgramAddress:], emulator.ROM.Data) } func (emulator *Emulator) LoadFont() { copy(emulator.Memory[0:], []uint8{ 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 }) } func (emulator *Emulator) Update() { emulator.UpdateTimers() for cycle := 0; cycle < CyclesPerFrame; cycle++ { emulator.Cycle() } } func (emulator *Emulator) UpdateTimers() { if emulator.ST != 0 { emulator.PlaySound() } else { emulator.StopSound() } emulator.ST = uint8(math.Max(0, float64(emulator.ST)-1)) emulator.DT = uint8(math.Max(0, float64(emulator.DT)-1)) } func (emulator *Emulator) Cycle() { instruction := binary.BigEndian.Uint16(emulator.Memory[emulator.PC:]) if instruction == 0x0000 { return } emulator.PC += InstructionSize nnn := instruction & 0x0FFF n := uint8(instruction & 0x000F) kk := uint8(instruction & 0x00FF) x := uint8(instruction & 0x0F00 >> 8) y := uint8(instruction & 0x00F0 >> 4) switch instruction >> 12 & 0xF { case 0x0: switch instruction { case 0x00E0: // 00E0 - CLS emulator.ClearScreen() case 0x00EE: // 00EE - RET emulator.Return() } case 0x1: // 1nnn - JP addr emulator.Jump(nnn) case 0x2: // 2nnn - CALL addr emulator.Call(nnn) case 0x3: // 3xkk - SE Vx, byte emulator.SkipEqualByte(x, kk) case 0x4: // 4xkk - SNE Vx, byte emulator.SkipNotEqualByte(x, kk) case 0x5: // 5xy0 - SE Vx, Vy emulator.SkipEqual(x, y) case 0x6: // 6xkk - LD Vx, byte emulator.LoadByte(x, kk) case 0x7: // 7xkk - ADD Vx, byte emulator.AddByte(x, kk) case 0x8: switch instruction & 0x000F { case 0x0: // 8xy0 - LD Vx, Vy emulator.Load(x, y) case 0x1: // 8xy1 - OR Vx, Vy emulator.Or(x, y) case 0x2: // 8xy2 - AND Vx, Vy emulator.And(x, y) case 0x3: // 8xy3 - XOR Vx, Vy emulator.Xor(x, y) case 0x4: // 8xy4 - ADD Vx, Vy emulator.Add(x, y) case 0x5: // 8xy5 - SUB Vx, Vy emulator.Sub(x, y) case 0x6: // 8xy6 - SHR Vx {, Vy} emulator.ShiftRight(x) case 0x7: // 8xy7 - SUBN Vx, Vy emulator.SubN(x, y) case 0xE: // 8xyE - SHL Vx {, Vy} emulator.ShiftLeft(x) } case 0x9: // 9xy0 - SNE Vx, Vy emulator.SkipNotEqual(x, y) case 0xA: // Annn - LD I, nnn emulator.LoadI(nnn) case 0xB: // Bnnn - JP V0, nnn emulator.JumpV0(nnn) case 0xC: // Cxkk - RND Vx, kk emulator.Random(x, kk) case 0xD: // Dxyn - DRW Vx, Vy, n emulator.Draw(x, y, n) case 0xE: switch instruction & 0x00FF { case 0x9E: // SKP Vx emulator.SkipKeyPressed(x) case 0xA1: // SKNP Vx emulator.SkipKeyNotPressed(x) } case 0xF: switch instruction & 0x00FF { case 0x07: // LD Vx, DT emulator.ReadDT(x) case 0x0A: // LD Vx, K emulator.ReadKey(x) case 0x15: // LD DT, Vx emulator.SetDT(x) case 0x18: // LD ST, Vx emulator.SetST(x) case 0x1E: // ADD I, Vx emulator.AddI(x) case 0x29: // LD F, Vx emulator.SetI(x) case 0x33: // LD B, Vx emulator.LoadBCD(x) case 0x55: // LD [I], Vx emulator.StoreRegisters(x) case 0x65: // LD Vx, [I] emulator.ReadRegisters(x) } } }