diff --git a/emulator/chip8.go b/emulator/chip8.go index 0dad9c6..ba13e5e 100644 --- a/emulator/chip8.go +++ b/emulator/chip8.go @@ -1,39 +1,46 @@ package emulator -import "errors" +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 { - memory [4096]byte - v [16]byte - i uint16 - pc uint16 - sp uint16 - delayTimer uint8 - soundTimer uint8 - stack [16]uint16 - keypad [16]bool - // To transform the linear Memory to a 2D array for easier drawing, - // having a (x, y) coordinate then (x, y) = graphics[x + (y * 64)] - graphics [64 * 32]bool - opcode uint16 - fonts [80]byte + state *CHIP8State + // A flag to check if the screen needs to be redrawn + DrawFlag bool } func NewChip8() *CHIP8 { chip8 := &CHIP8{ - memory: [4096]byte{}, - v: [16]byte{}, - i: 0, - // The original 0x200 code of the CHIP-8 interpreter was from 0x0 to 0x200, - pc: 0x200, - sp: 0, - delayTimer: 0, - soundTimer: 0, - stack: [16]uint16{}, - keypad: [16]bool{}, - graphics: [64 * 32]bool{}, - opcode: 0, - fonts: [80]byte{}, + 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 @@ -61,17 +68,121 @@ func (c *CHIP8) Initialize() { // Char map goes from 0x050 to 0x9F for i, charByte := range charMap { - c.memory[i+0x50] = charByte + 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.memory)-0x200 { + 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.memory[i+0x200] = datByte + 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") + } +} diff --git a/go.mod b/go.mod index fe3d92f..4aa1273 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/micosilent/go_chip8 go 1.22.3 -require github.com/hajimehoshi/ebiten/v2 v2.7.7 +require ( + github.com/hajimehoshi/ebiten/v2 v2.7.7 + github.com/gin-gonic/gin v1.10.0 +) require ( github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect diff --git a/go.sum b/go.sum index f881c41..5199479 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc= github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/hajimehoshi/ebiten/v2 v2.7.7 h1:FyiuIOZqKU4aefYVws/lBDhTZu2WY2m/eWI3PtXZaHs= github.com/hajimehoshi/ebiten/v2 v2.7.7/go.mod h1:Ulbq5xDmdx47P24EJ+Mb31Zps7vQq+guieG9mghQUaA= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= diff --git a/main.go b/main.go index f9a1d15..babbb8f 100644 --- a/main.go +++ b/main.go @@ -5,22 +5,59 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/micosilent/go_chip8/emulator" + "image/color" "log" "os" "time" ) +const ( + screenWidth = 640 + screenHeight = 320 + gridWidth = 64 + gridHeight = 32 +) + type Game struct { counter int lastFrameTime time.Time debugText string emulator *emulator.CHIP8 + screenImage *ebiten.Image } func (g *Game) Update() error { + + // We need the emulator to process around 700 instructions per second + for i := 0; i < 1; i++ { + g.emulator.EmulateCycle() + if g.emulator.DrawFlag { + g.screenImage = g.DrawPixels() + g.emulator.DrawFlag = false + } + } return nil } +func (g *Game) DrawPixels() *ebiten.Image { + img := ebiten.NewImage(gridWidth, gridHeight) + white := color.White + black := color.Black + + // Draw the boolean array to the image + for x := 0; x < gridWidth; x++ { + for y := 0; y < gridHeight; y++ { + if g.emulator.GetPixelAtCoordinate(x, y) { + img.Set(x, y, white) + } else { + img.Set(x, y, black) + } + } + } + + return img +} + func newGame() *Game { return &Game{ counter: 1, @@ -43,10 +80,17 @@ func (g *Game) Draw(screen *ebiten.Image) { ebitenutil.DebugPrint(screen, g.debugText) g.counter++ g.lastFrameTime = time.Now() + + scaleX := float64(screenWidth) / float64(gridWidth) + scaleY := float64(screenHeight) / float64(gridHeight) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Scale(scaleX, scaleY) + screen.DrawImage(g.screenImage, op) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { - return 320, 240 + return screenWidth, screenHeight } func (g *Game) LoadROM(path string) { @@ -60,7 +104,7 @@ func (g *Game) LoadROM(path string) { func main() { game := newGame() game.LoadROM("roms/ibm_logo.ch8") - ebiten.SetWindowSize(480, 320) + ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowTitle("Go CHIP8 Emulator") err := ebiten.RunGame(game) diff --git a/roms/ibm_logo_dissasembly.md b/roms/ibm_logo_dissasembly.md new file mode 100644 index 0000000..04741e4 --- /dev/null +++ b/roms/ibm_logo_dissasembly.md @@ -0,0 +1,66 @@ +## IBM Logo program + +Theorically we only need these instructions to decode the whole program: + +- 00E0 (clear screen) +- 1NNN (jump) +- 6XNN (set register VX) +- 7XNN (add value to register VX) +- ANNN (set index register I) +- DXYN (display/draw) + - This is the most involved instruction. It will draw an N pixels tall sprite + from the memory location that the I index register is holding to the screen, + at the horizontal X coordinate in VX and the Y coordinate in VY. All the pixels + that are “on” in the sprite will flip the pixels on the screen that it is drawn + to (from left to right, from most to least significant bit). If any pixels on + the screen were turned “off” by this, the VF flag register is set to 1. + Otherwise, it’s set to 0. + +## Binary +1. 00E0 Clear SCREEN +2. A22A Set Register I to 0x22A +3. 600C Set Register 0 to 0xC +4. 6108 Set Register 1 to 0x8 +5. D01F Draw + - Register[0] = 0xC + - Register[1] = 0x8 + - N ( how tall is the sprite ) = 0xF + - Register[I] = 0x22A +6. 7009 Add 0x9 to Register 0 (new value is 0x15) +7. A239 Set Register I to 0x239 +8. D01F Draw + - Register[0] = 0x15 + - Register[1] = 0x8 + - N ( how tall is the sprite ) = 0xF + - Register[I] = 0x239 +9. A248 Set Register I to 0x248 +10. 7008 Add 0x8 to Register 0 (new value is 0x1D) +11. D01F Draw + - Register[0] = 0x1D + - Register[1] = 0x8 + - N ( how tall is the sprite ) = 0xF + - Register[I] = 0x248 +12. 7004 Add 0x4 to Register 0 (new value is 0x21) +13. A257 Set Register I to 0x257 +14. D01F Draw + - Register[0] = 0x21 + - Register[1] = 0x8 + - N ( how tall is the sprite ) = 0xF + - Register[I] = 0x257 +15. 7008 +16. A266 Set Register I to 0x266 +17. D01F Draw + - Register[0] = 0x21 + - Register[1] = 0x8 + - N ( how tall is the sprite ) = 0xF + Register[I] = 0x266 +18. 7008 Add 0x8 to Register 0 (new value is 0x29) +19. A275 Set Register I to 0x275 +20. D01F Draw + - Register[0] = 0x29 + - Register[1] = 0x8 + - N ( how tall is the sprite ) = 0xF + - Register[I] = 0x275 +21. 1228 Set program counter to 0x228 +22. From this point onwards I think it is sprite data +