diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..6d8c856 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,53 @@ +kind: pipeline +type: docker +name: default + +steps: +- name : test + image: golang:latest + commands: + - go test -v ./chip8 +- name : build windows + image: golang:latest + commands: + - go build -o windows_test ./cmd/test_prog + enviroment: + GOOS: windows + GOARCH: amd64 +- name : build linux + image: golang:latest + commands: + - go build -o linux_test ./cmd/test_prog + enviroment: + GOOS: linux + GOARCH: amd64 +- name : build mac + image: golang:latest + commands: + - go build -o mac_test ./cmd/test_prog + enviroment: + GOOS: darwin + GOARCH: amd64 + + +- name: publish + image: plugins/gitea-release + depends_on: + - test + - build windows + - build linux + - build mac + # This step is only run when a branch is tagged in Gitea. + when: + event: + - tag + settings: + base_url: https://git.jacknet.io + api_key: + from_secret: gitea_token + files: + - mac_test + - linux_test + - windows_test + checksum: + - sha1 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..88178bd --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +[![Build Status](https://drone.jacknet.io/api/badges/S.D/Chip-8_Go/status.svg)](https://drone.jacknet.io/S.D/Chip-8_Go) \ No newline at end of file diff --git a/chip8/chip8.go b/chip8/chip8.go new file mode 100644 index 0000000..02e513d --- /dev/null +++ b/chip8/chip8.go @@ -0,0 +1,305 @@ +package chip8 + +import ( + "fmt" + "math/rand" +) + +const graphicsBufferSize = 64 * 32 + +type Chip8 struct { + addressRegister uint16 + beepTimer byte + drawRequired bool + delayTimer byte + graphics [graphicsBufferSize]byte + keys [16]byte + memory [4096]byte + opcode uint16 + pc uint16 + registers [16]byte // an array - has a fixed length + stack []uint16 // a slice - basically a c++ vector +} + +// we can't have const arrays in go +// This is a good a solution as any +func getLetters() []byte { + return []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 +} + +func NewCHIP8(prog []byte) *Chip8 { + cpu := Chip8{pc: 0x200} + memory_slice := cpu.memory[:80] + copy(memory_slice, getLetters()) + memory_slice = cpu.memory[200:] + copy(memory_slice, prog) + + // Do some extra checking to ensure the right + // Stuff is copied + + return &cpu +} + +func (cpu *Chip8) GetGraphicsBuffer() [graphicsBufferSize]byte { + return cpu.graphics +} + +func (cpu *Chip8) clearDisplay() { + // fuck it the gc can do the hard work for us + cpu.graphics = [64 * 32]byte{} + cpu.drawRequired = true +} + +// what if there's nothing in the stack? +// return something sensible +func (cpu *Chip8) leaveFunction() { + cpu.pc = cpu.stack[len(cpu.stack)-1] + cpu.stack[len(cpu.stack)-1] = 0 + cpu.stack = cpu.stack[:len(cpu.stack)-1] +} + +func (cpu *Chip8) goTo() { + cpu.pc = cpu.opcode & 0x0FFF +} + +func (cpu *Chip8) callSubroutine() { + cpu.stack = append(cpu.stack, cpu.pc) + cpu.pc = cpu.opcode & 0x0FFF +} + +func (cpu *Chip8) skipIfRegisterEqual() { + r := (cpu.opcode) >> 8 & 0x0F + if cpu.registers[r] == byte(cpu.opcode&0xFF) { + cpu.pc += 2 + } +} + +func (cpu *Chip8) skipIfRegisterNotEqual() { + r := (cpu.opcode) >> 8 & 0x0F + if cpu.registers[r] != byte(cpu.opcode&0xFF) { + cpu.pc += 2 + } +} + +func (cpu *Chip8) skipIfRegistersEqual() { + x, y := (cpu.opcode>>8)&0x0F, (cpu.opcode>>4)&0x0F + if cpu.registers[x] == cpu.registers[y] { + cpu.pc += 2 + } +} + +func (cpu *Chip8) setRegisterTo() { + r := (cpu.opcode >> 8) & 0x0F + cpu.registers[r] = byte(cpu.opcode & 0xFF) +} + +func (cpu *Chip8) registerPlusEqualsNN() {} // QUESTION HERE - WHAT DO IF IT WOULD WRAP ROUND? + +func (cpu *Chip8) bitOpsAndMath() { + instruction := cpu.opcode & 0x0F + regX, regY := cpu.opcode>>8&0x0F, cpu.opcode>>4&0x0F + switch instruction { + case 0: + // 8XY0 + // Assign value of register Y to register X + cpu.registers[regX] = cpu.registers[regY] + case 1: + // 8XY1 + // Set register X to x|Y + cpu.registers[regX] = cpu.registers[regX] | cpu.registers[regY] + case 2: + // 8XY2 + // Set register x to X&Y + cpu.registers[regX] = cpu.registers[regX] & cpu.registers[regY] + case 3: + // 8XY3 + // Set register x to X^Y + cpu.registers[regX] = cpu.registers[regX] ^ cpu.registers[regY] + case 4: + // 8XY4 + // Set register x to X+=Y (Set VF to 1 when there's a carry and 0 if not) + res := cpu.registers[regX] + cpu.registers[regY] + if res > 0xFF { + cpu.registers[0x0F] = 1 + } else { + cpu.registers[0x0F] = 0 + } + cpu.registers[regX] = res & 0xFF + case 5: + // 8XY5 + // Set register x to X-=Y (Set VF to 1 when there's a carry and 0 if not) + res := cpu.registers[regX] - cpu.registers[regY] + if res > 0xFF { + cpu.registers[0x0F] = 1 + } else { + cpu.registers[0x0F] = 0 + } + cpu.registers[regX] = res & 0xFF + case 6: + // BXY6 + // Store lsb of reg X in reg F and then shift reg X >> 1 + cpu.registers[0x0F] = cpu.registers[regX] & 0x01 + cpu.registers[regX] >>= 1 + case 7: + // 8XY57 + // Set register x to X=Y-X (Set VF to 1 when there's a carry and 0 if not) + res := cpu.registers[regY] - cpu.registers[regX] + if res < 0 { + cpu.registers[0x0F] = 0 + } else { + cpu.registers[0x0f] = 1 + } + cpu.registers[regX] = res & 0xFF + } +} + +func (cpu *Chip8) skipIfRegistersNotEqual() { + x, y := (cpu.opcode>>8)&0x0F, (cpu.opcode>>4)&0x0F + if cpu.registers[x] != cpu.registers[y] { + cpu.pc += 2 + } +} + +func (cpu *Chip8) setAddressRegister() { + // ANNN + // Sets the address register to NNN + cpu.addressRegister = cpu.opcode & 0x0FFF +} + +func (cpu *Chip8) jumpToV0PlusAddress() { + // BNNN + // PC=V0+NNN + cpu.pc = uint16(cpu.registers[0]) + (cpu.opcode & 0x0FFF) +} + +func (cpu *Chip8) setRegisterToRand() { + // CXNN + // Vx = rand() & NN + cpu.registers[(cpu.opcode>>8)&0x0F] = byte(cpu.opcode&0xFF) & byte(rand.Intn(256)) +} + +func (cpu *Chip8) displaySprite() { + // DXYN + // Draws a sprite in the graphics buffer + // VF is set to 1 if any pixels are flipped and zero otherwise + cpu.registers[0x0F] = 0 + x := int(cpu.registers[(cpu.opcode>>8)&0x0F]) + y := int(cpu.registers[(cpu.opcode>>4)&0x0F]) + + for row := 0; row < int(cpu.opcode&0xF); row++ { + pixel := cpu.memory[int(cpu.addressRegister)+row] + for column := 0; column < 8; column++ { + if pixel&(0x80>>column) != 0 { + graphicsPosition := x + column + ((y + row) * 60) + if cpu.graphics[graphicsPosition] == 1 { + cpu.registers[0xF] = 1 + } + cpu.graphics[graphicsPosition] ^= 1 + } + } + } + cpu.drawRequired = true +} + +func (cpu *Chip8) SkipOnKeyOpcodes() { + opcode := cpu.opcode & 0xFF + key := cpu.registers[(cpu.opcode>>8)&0x0F] + switch opcode { + case 0x9E: + // EX9E + // skip if key X is pressed + if cpu.keys[key] != 0 { + cpu.pc += 2 + } + case 0xA1: + // EXA1 + // skip if kkey X is not pressed + if cpu.keys[key] == 0 { + cpu.pc += 2 + } + } +} + +func (cpu *Chip8) FifteenIndexOpcodes() { + instruction := cpu.opcode & 0xFF + reg := int(cpu.opcode>>8) & 0xF + switch instruction { + // FX07 + // Set VX to the value of the delay timer + case 0x07: + cpu.registers[reg] = cpu.delayTimer + case 0x0A: + // FX0A + // block + wait for key press + for i, val := range cpu.keys { + if val != 0 { + cpu.registers[reg] = byte(i) + } + } + case 0x15: + // FX15 + // Set delay timer to VX + cpu.delayTimer = cpu.registers[reg] + case 0x18: + // FX18 + // SET THE BEEP TIMER TO VX + // BEEP + cpu.beepTimer = cpu.registers[reg] + case 0x1E: + // FX1E + // Add VX to the address register + // Set VF to 1 when range overflow + if int(cpu.registers[reg])+int(cpu.addressRegister) > 0xFFF { + cpu.registers[0x0F] = 1 + } else { + cpu.registers[0x0F] = 0 + } + cpu.addressRegister = uint16((int(cpu.addressRegister) + int(cpu.registers[reg])) & 0xFFF) + case 0x29: + // FX29 + // Sets address register to location of char stored in VX + cpu.addressRegister = uint16(cpu.registers[reg]) * 5 + case 0x33: + // FX33 + // Stores BCD representation of VX + cpu.memory[cpu.addressRegister] = cpu.registers[reg] / 100 + cpu.memory[cpu.addressRegister+1] = (cpu.registers[reg] / 10) % 10 + cpu.memory[cpu.addressRegister+2] = cpu.registers[reg] % 10 + case 0x55: + // FX55 + // takes values from and including reg X and stores then in memory + for i := 0; i <= reg; i++ { + cpu.memory[int(cpu.addressRegister)+i] = cpu.registers[i] + } + case 0x65: + // FX65 + // takes values from memory and stores them in registers + for i := 0; i <= reg; i++ { + cpu.registers[i] = cpu.memory[int(cpu.addressRegister)+i] + } + } + +} + +func main() { + fmt.Printf("Hello world!\n") + prog := []byte{1, 2, 3, 4} + new_cpu := NewCHIP8(prog) + fmt.Printf("%d\n", new_cpu.opcode) +} diff --git a/chip8/chip8_test.go b/chip8/chip8_test.go new file mode 100644 index 0000000..68329d4 --- /dev/null +++ b/chip8/chip8_test.go @@ -0,0 +1,149 @@ +package chip8 + +import ( + "fmt" + "testing" +) + +func slicesEqual(x, y []byte) bool { + if len(x) != len(y) { + return false + } + for i, xi := range x { + if xi != y[i] { + return false + } + } + return true +} + +// This test is kinda shit, there's so much stuff we ain't testing +// Maybe fix +func TestCreateCPU(t *testing.T) { + prog := []byte{1, 2, 3, 4} + newCPU := NewCHIP8(prog) + if !slicesEqual(newCPU.memory[200:204], prog) { + t.Errorf("CPU not initalized properly") + } +} + +func TestClearDisplay(t *testing.T) { + cpu := Chip8{} + for i := range cpu.graphics { + cpu.graphics[i] = byte(i % 255) + } + cpu.clearDisplay() + graphicsArray := cpu.GetGraphicsBuffer() + graphicsSlice := graphicsArray[:] + emptySlice := make([]byte, len(cpu.graphics)) + if !slicesEqual(graphicsSlice, emptySlice) { + t.Errorf("Graphics buffer not cleared properly") + } +} + +func TestLeaveFunction(t *testing.T) { + cpu := Chip8{pc: 50} + cpu.stack = append(cpu.stack, 1, 2, 3, 4, 5) + cpu.leaveFunction() + if cpu.pc != 4 && len(cpu.stack) != 4 { + t.Errorf("TestLeaveFunction not in expected state") + } +} + +func TestGoTo(t *testing.T) { + cpu := Chip8{opcode: 0x3420} + cpu.goTo() + if cpu.pc != 0x420 { + t.Errorf("Test GoTo not working as expected") + } +} + +func TestCallSubroutine(t *testing.T) { + cpu := Chip8{opcode: 0x3420, pc: 43} + cpu.callSubroutine() + if (cpu.pc != 420) && (cpu.stack[0] != 43) && (len(cpu.stack) != 1) { + t.Errorf("CallSubroutine not working as expected") + } +} + +func TestSkipIfRegisterEqual(t *testing.T) { + var tests = []struct { + register, regValue byte + compValue, want uint16 + }{ + {0, 2, 4, 0}, + {0, 2, 2, 2}, + {15, 2, 2, 2}, + {15, 1, 12, 0}, + } + + for _, tt := range tests { + testname := fmt.Sprintf("Reg:%d,RegVal:%d,Comp:%d", tt.register, tt.regValue, tt.compValue) + t.Run(testname, func(t *testing.T) { + cpu := Chip8{opcode: tt.compValue + uint16(tt.register)<<8} + cpu.registers[tt.register] = tt.regValue + cpu.skipIfRegisterEqual() + if tt.want != cpu.pc { + t.Errorf("PC is %d, Wanted %d", cpu.pc, tt.want) + } + }) + } +} + +func TestSkipIfRegisterNotEqual(t *testing.T) { + var tests = []struct { + register, regValue byte + compValue, want uint16 + }{ + {0, 2, 4, 2}, + {0, 2, 2, 0}, + {15, 2, 2, 0}, + {15, 1, 12, 2}, + } + + for _, tt := range tests { + testname := fmt.Sprintf("Reg:%d,RegVal:%d,Comp:%d", tt.register, tt.regValue, tt.compValue) + t.Run(testname, func(t *testing.T) { + cpu := Chip8{opcode: tt.compValue + uint16(tt.register)<<8} + cpu.registers[tt.register] = tt.regValue + cpu.skipIfRegisterNotEqual() + if tt.want != cpu.pc { + t.Errorf("PC is %d, Wanted %d", cpu.pc, tt.want) + } + }) + } +} + +func TestSkipIfRegistersEqual(t *testing.T) { + var tests = []struct { + reg1, reg2, val1, val2 byte + }{ + {0, 2, 4, 2}, + {0, 2, 2, 2}, + {15, 1, 2, 0}, + {15, 4, 12, 12}, + } + + for _, tt := range tests { + testname := fmt.Sprintf("Reg1:%d,Reg2:%d,val1:%d,val2:%d", tt.reg1, tt.reg2, tt.val1, tt.val2) + t.Run(testname, func(t *testing.T) { + cpu := Chip8{opcode: uint16(tt.reg1)<<8 + uint16(tt.reg2)<<4} + cpu.registers[tt.reg1] = tt.val1 + cpu.registers[tt.reg2] = tt.val2 + cpu.skipIfRegistersEqual() + if (2 != cpu.pc) && (tt.val1 == tt.val2) { + t.Errorf("PC is %d, Wanted %d", cpu.pc, 2) + } else if (0 != cpu.pc) && (tt.val1 != tt.val2) { + t.Errorf("PC is %d, Wanted %d", cpu.pc, 2) + } + }) + } +} + +func TestSetRegisterTo(t *testing.T) { + cpu := Chip8{opcode: 0x0824} + cpu.setRegisterTo() + if cpu.registers[8] != 0x24 { + t.Errorf("Register 8 is %d wanted %d", cpu.registers[8], 0x24) + } +} diff --git a/cmd/test_prog/main.go b/cmd/test_prog/main.go new file mode 100644 index 0000000..21e8d6d --- /dev/null +++ b/cmd/test_prog/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "git.jacknet.io/S.D/Chip-8_Go/chip8" + "fmt" + ) + +func main() { + prog := make([]byte, 6) + cpu := chip8.NewCHIP8(prog) + fmt.Printf("This should print out zero: %d!\n", cpu.GetGraphicsBuffer()[0]) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a927f9a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.jacknet.io/S.D/Chip-8_Go + +go 1.15