package emulator import ( "encoding/json" "errors" "fmt" ) type CHIP8State struct { Memory [4096]byte `json:"memory"` V [16]byte `json:"v"` I uint16 `json:"i"` ProgramCounter uint16 `json:"program_counter"` StackPointer uint16 `json:"stack_pointer"` DelayTimer uint8 `json:"delay_timer"` SoundTimer uint8 `json:"sound_timer"` Stack [16]uint16 `json:"stack"` Keypad [16]bool `json:"keypad"` Graphics [64 * 32]bool `json:"graphics"` } type CHIP8 struct { state *CHIP8State // A flag to check if the screen needs to be redrawn DrawFlag bool } func NewChip8() *CHIP8 { chip8 := &CHIP8{ state: &CHIP8State{ Memory: [4096]byte{}, V: [16]byte{}, I: 0, // The original 0x200 code of the CHIP-8 interpreter was from 0x0 to 0x200, ProgramCounter: 0x200, StackPointer: 0, DelayTimer: 0, SoundTimer: 0, Stack: [16]uint16{}, Keypad: [16]bool{}, Graphics: [64 * 32]bool{false}, }, DrawFlag: true, } chip8.Initialize() return chip8 } func (c *CHIP8) Initialize() { charMap := [80]byte{ 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 } // Char map goes from 0x050 to 0x9F for i, charByte := range charMap { c.state.Memory[i+0x50] = charByte } } func (c *CHIP8) StateToJSON() string { jsonData, err := json.Marshal(c.state) if err != nil { fmt.Println("Something went wrong marshalling CHIP8 state to JSON") return "" } return string(jsonData) } func (c *CHIP8) LoadROMIntoMemory(dat []byte) error { if len(dat) > len(c.state.Memory)-0x200 { return errors.New("ROM is too large to fit into memory") } // Roms start from 0x200 for i, datByte := range dat { c.state.Memory[i+0x200] = datByte } return nil } func (c *CHIP8) GetPixelAtCoordinate(x, y int) bool { return c.state.Graphics[x+y*64] } func (c *CHIP8) setPixelAtCoordinate(x, y int, newState bool) { c.state.Graphics[x+y*64] = newState } func (c *CHIP8) EmulateCycle() { /* Fetch an instruction from the memory that the PC is currently pointing to. An instruction is 2 bytes long and the CHIP8 is big endian. So we fetch two consecutive bytes, and increment the PC by 2. */ instruction := uint16(c.state.Memory[c.state.ProgramCounter])<<8 | uint16(c.state.Memory[c.state.ProgramCounter+1]) c.state.ProgramCounter += 2 /* The first four bits of the instruction represent the type of operation. And the rest of the bits represent the operands. So we mask the first four bits, and then use that as an index to our instructionSet. We also mask some extra nibbles to have them ready. */ // X, The second nibble, represents one of the 16 registers x := (instruction & 0x0F00) >> 8 // Y, The third nibble, represents one of the 16 registers y := (instruction & 0x00F0) >> 4 // N, The least significant nibble, represents a number between 0 and 0xF, 4 bit number n := instruction & 0x000F // NN, The two least significant bytes of the instruction, representing a 16-bit number nn := byte(instruction & 0x00FF) // NNN, The three least significant bytes of the instruction, representing a 16-bit number nnn := instruction & 0x0FFF maskedInstruction := instruction & 0xF000 if instruction == 0x00E0 { // Clear the screen c.state.Graphics = [64 * 32]bool{false} c.DrawFlag = true } else if maskedInstruction == 0x1000 { // Jump to a new location c.state.ProgramCounter = nnn } else if maskedInstruction == 0x2000 { // Call a subroutine at a given address, storing the current PC on the stack c.state.Stack[c.state.StackPointer] = c.state.ProgramCounter c.state.StackPointer++ c.state.ProgramCounter = nnn } else if instruction == 0x00EE { // Return from a subroutine c.state.StackPointer-- c.state.ProgramCounter = c.state.Stack[c.state.StackPointer] } else if maskedInstruction == 0x6000 { // Set VX to NN c.state.V[x] = nn } else if maskedInstruction == 0x7000 { // Add NN to VX, and store the result in VX c.state.V[x] += nn } else if maskedInstruction == 0xA000 { // Set I to the address NNN c.state.I = nnn } else if maskedInstruction == 0xD000 { xCoord := int(c.state.V[x] % 64) yCoord := int(c.state.V[y] % 32) c.state.V[0xF] = 0 for rowIndex := 0; rowIndex < int(n); rowIndex++ { spriteData := c.state.Memory[int(c.state.I)+rowIndex] for colIndex := 0; colIndex < 8; colIndex++ { spritePixel := (spriteData & (1 << (7 - colIndex))) >> (7 - colIndex) spritePixelBool := spritePixel == 1 screenPixel := c.GetPixelAtCoordinate(xCoord+colIndex, yCoord+rowIndex) if spritePixelBool && screenPixel { c.setPixelAtCoordinate(xCoord+colIndex, yCoord+rowIndex, false) c.state.V[0xF] = 1 } else if spritePixelBool { c.setPixelAtCoordinate(xCoord+colIndex, yCoord+rowIndex, true) } if colIndex >= 64 { break } } if rowIndex >= 32 { break } } c.DrawFlag = true } else { panic("uff") } }