feat: minimal instruction set for IBM logo program
parent
ba28865248
commit
1e06ca350a
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
go.mod
5
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
|
||||
|
|
|
|||
1
go.sum
1
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=
|
||||
|
|
|
|||
48
main.go
48
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
Loading…
Reference in New Issue