go_chip8/emulator/chip8.go

189 lines
5.5 KiB
Go

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")
}
}