189 lines
5.5 KiB
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")
|
|
}
|
|
}
|