diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6550511..1f7e517 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,20 @@ jobs: build: runs-on: ubuntu-latest + env: + DISPLAY: ':99.0' + steps: + - name: Install ebiten build dependencies + run: | + sudo apt install gcc libc6-dev libgl1-mesa-dev libxcursor-dev \ + libxi-dev libxinerama-dev libxrandr-dev \ + libxxf86vm-dev libasound2-dev pkg-config + + - name: Run Xvfb for ebitin + run: | + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + - uses: actions/checkout@v3 with: submodules: true diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 47d3f90..c1bdbfc 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -13,6 +13,12 @@ jobs: name: lint runs-on: ubuntu-latest steps: + - name: Install ebiten build dependencies + run: | + sudo apt install gcc libc6-dev libgl1-mesa-dev libxcursor-dev \ + libxi-dev libxinerama-dev libxrandr-dev \ + libxxf86vm-dev libasound2-dev pkg-config + - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..cde904a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,3 @@ +--- +run: + modules-download-mode: mod diff --git a/Makefile b/Makefile index 72aa924..78a3a63 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ help: .PHONY: tidy tidy: - go fmt ./... + go fmt -mod=mod ./... go mod tidy -v .PHONY: build @@ -29,15 +29,15 @@ clean: .PHONY: run run: - $(GO) run . + $(GO) run -mod=mod . .PHONY: test test: - $(GO) test -v ./... + $(GO) test -mod=mod -v ./... .PHONY: bin/gogo-gb # This does exist, but we're not tracking its dependencies. Go is bin/gogo-gb: - $(GO) build -o bin/gogo-gb . + $(GO) build -mod=mod -o bin/gogo-gb . .PHONY: cpu_instrs cpu_instrs: bin/gogo-gb vendor/gameboy-doctor/gameboy-doctor vendor/gb-test-roms/cpu_instrs/individual/*.gb diff --git a/devices/host.go b/devices/host.go deleted file mode 100644 index 891c8dc..0000000 --- a/devices/host.go +++ /dev/null @@ -1,53 +0,0 @@ -package devices - -import ( - "log" -) - -type HostContext interface { - Logger() *log.Logger - Log(msg string, args ...any) - LogErr(msg string, args ...any) - LogWarn(msg string, args ...any) - SerialCable() SerialCable -} - -type Host struct { - logger *log.Logger - serialCable SerialCable -} - -func NewHost() *Host { - return &Host{ - logger: log.Default(), - serialCable: &NullSerialCable{}, - } -} - -func (h *Host) Logger() *log.Logger { - return h.logger -} - -func (h *Host) Log(msg string, args ...any) { - h.logger.Printf(msg+"\n", args...) -} - -func (h *Host) LogErr(msg string, args ...any) { - h.Log("ERROR: "+msg, args...) -} - -func (h *Host) LogWarn(msg string, args ...any) { - h.Log("WARN: "+msg, args...) -} - -func (h *Host) SetLogger(logger *log.Logger) { - h.logger = logger -} - -func (h *Host) SerialCable() SerialCable { - return h.serialCable -} - -func (h *Host) AttachSerialCable(serialCable SerialCable) { - h.serialCable = serialCable -} diff --git a/devices/host_interface.go b/devices/host_interface.go new file mode 100644 index 0000000..2dbc019 --- /dev/null +++ b/devices/host_interface.go @@ -0,0 +1,12 @@ +package devices + +import "image" + +type HostInterface interface { + Framebuffer() chan<- image.Image + Log(msg string, args ...any) + LogErr(msg string, args ...any) + LogWarn(msg string, args ...any) + Exited() <-chan bool + SerialCable() SerialCable +} diff --git a/devices/lcd.go b/devices/lcd.go index 8a2b7b7..3f5b537 100644 --- a/devices/lcd.go +++ b/devices/lcd.go @@ -1,6 +1,11 @@ package devices -import "github.com/maxfierke/gogo-gb/mem" +import ( + "image" + "image/color" + + "github.com/maxfierke/gogo-gb/mem" +) const ( REG_LCD_LY = 0xFF44 @@ -13,6 +18,16 @@ func NewLCD() *LCD { return &LCD{} } +func (lcd *LCD) Draw() image.Image { + image := image.NewPaletted( + image.Rect(0, 0, 160, 144), + color.Palette{color.Black, color.Gray{Y: 96}, color.Gray16{Y: 128}, color.White}, + ) + image.Set(80, 77, color.Gray{Y: 96}) + + return image +} + func (lcd *LCD) OnRead(mmu *mem.MMU, addr uint16) mem.MemRead { return mem.ReadPassthrough() } diff --git a/devices/serial_port.go b/devices/serial_port.go index 0e7e529..9ebfca7 100644 --- a/devices/serial_port.go +++ b/devices/serial_port.go @@ -73,19 +73,23 @@ func (sc *SerialCtrl) SetClockInternal(enabled bool) { } type SerialPort struct { - clk uint - ctrl SerialCtrl - recv byte - buf byte - host HostContext + clk uint + ctrl SerialCtrl + recv byte + buf byte + cable SerialCable } -func NewSerialPort(host HostContext) *SerialPort { +func NewSerialPort() *SerialPort { return &SerialPort{ - host: host, + cable: &NullSerialCable{}, } } +func (sp *SerialPort) AttachCable(cable SerialCable) { + sp.cable = cable +} + func (sp *SerialPort) Step(cycles uint8, ic *InterruptController) { if !sp.ctrl.IsTransferEnabled() { return @@ -122,18 +126,12 @@ func (sp *SerialPort) OnWrite(mmu *mem.MMU, addr uint16, value byte) mem.MemWrit sp.ctrl.Write(value) if sp.ctrl.IsTransferEnabled() && sp.ctrl.IsClockInternal() { - cable := sp.host.SerialCable() - logger := sp.host.Logger() - // TODO(GBC): derive this somehow and factor in GBC speeds when relevant sp.clk = 8192 - err := cable.WriteByte(sp.buf) - if err != nil { - logger.Printf("Unable to write 0x%02X to serial cable: %v\n", value, err) - } + _ = sp.cable.WriteByte(sp.buf) - recvVal, err := cable.ReadByte() + recvVal, err := sp.cable.ReadByte() if err != nil { sp.recv = 0xFF } else { diff --git a/go.mod b/go.mod index 1e03429..5d9f16b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,14 @@ module github.com/maxfierke/gogo-gb go 1.22.0 + +require github.com/hajimehoshi/ebiten/v2 v2.7.3 + +require ( + github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8 // indirect + github.com/ebitengine/hideconsole v1.0.0 // indirect + github.com/ebitengine/purego v0.7.0 // indirect + github.com/jezek/xgb v1.1.1 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..24f8c67 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8 h1:5e8X7WEdOWrjrKvgaWF6PRnDvJicfrkEnwAkWtMN74g= +github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8/go.mod h1:tWboRRNagZwwwis4QIgEFG1ZNFwBJ3LAhSLAXAAxobQ= +github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= +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/hajimehoshi/ebiten/v2 v2.7.3 h1:lDpj8KbmmjzwD19rsjXNkyelicu0XGvklZW6/tjrgNs= +github.com/hajimehoshi/ebiten/v2 v2.7.3/go.mod h1:1vjyPw+h3n30rfTOpIsbWRXSxZ0Oz1cYc6Tq/2DKoQg= +github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= +github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/hardware/console.go b/hardware/console.go new file mode 100644 index 0000000..1147abf --- /dev/null +++ b/hardware/console.go @@ -0,0 +1,15 @@ +package hardware + +import ( + "github.com/maxfierke/gogo-gb/cart" + "github.com/maxfierke/gogo-gb/debug" + "github.com/maxfierke/gogo-gb/devices" +) + +type Console interface { + AttachDebugger(debugger debug.Debugger) + DetachDebugger() + LoadCartridge(r *cart.Reader) error + Step() error + Run(host devices.HostInterface) error +} diff --git a/hardware/dmg.go b/hardware/dmg.go index 76243dc..09ea52c 100644 --- a/hardware/dmg.go +++ b/hardware/dmg.go @@ -1,7 +1,9 @@ package hardware import ( + "fmt" "log" + "time" "github.com/maxfierke/gogo-gb/cart" "github.com/maxfierke/gogo-gb/cpu" @@ -12,6 +14,14 @@ import ( const DMG_RAM_SIZE = 0xFFFF + 1 +type DMGOption func(dmg *DMG) + +func WithDebugger(debugger debug.Debugger) DMGOption { + return func(dmg *DMG) { + dmg.AttachDebugger(debugger) + } +} + type DMG struct { // Components cpu *cpu.CPU @@ -23,59 +33,64 @@ type DMG struct { timer *devices.Timer // Non-components - debugger debug.Debugger - host devices.HostContext + debugger debug.Debugger + debuggerHandler mem.MemHandlerHandle } -func NewDMG(host devices.HostContext) (*DMG, error) { - debugger := debug.NewNullDebugger() - return NewDMGDebug(host, debugger) -} - -func NewDMGDebug(host devices.HostContext, debugger debug.Debugger) (*DMG, error) { +func NewDMG(opts ...DMGOption) (*DMG, error) { cpu, err := cpu.NewCPU() if err != nil { return nil, err } - cartridge := cart.NewCartridge() - ic := devices.NewInterruptController() - lcd := devices.NewLCD() - serial := devices.NewSerialPort(host) - timer := devices.NewTimer() - ram := make([]byte, DMG_RAM_SIZE) mmu := mem.NewMMU(ram) echo := mem.NewEchoRegion() unmapped := mem.NewUnmappedRegion() - mmu.AddHandler(mem.MemRegion{Start: 0x0000, End: 0xFFFF}, debugger) + dmg := &DMG{ + cpu: cpu, + mmu: mmu, + cartridge: cart.NewCartridge(), + debugger: debug.NewNullDebugger(), + ic: devices.NewInterruptController(), + lcd: devices.NewLCD(), + serial: devices.NewSerialPort(), + timer: devices.NewTimer(), + } - mmu.AddHandler(mem.MemRegion{Start: 0x0000, End: 0x7FFF}, cartridge) // MBCs ROM Banks - mmu.AddHandler(mem.MemRegion{Start: 0xA000, End: 0xBFFF}, cartridge) // MBCs RAM Banks + for _, opt := range opts { + opt(dmg) + } + + mmu.AddHandler(mem.MemRegion{Start: 0x0000, End: 0x7FFF}, dmg.cartridge) // MBCs ROM Banks + mmu.AddHandler(mem.MemRegion{Start: 0xA000, End: 0xBFFF}, dmg.cartridge) // MBCs RAM Banks mmu.AddHandler(mem.MemRegion{Start: 0xE000, End: 0xFDFF}, echo) // Echo RAM (mirrors WRAM) mmu.AddHandler(mem.MemRegion{Start: 0xFEA0, End: 0xFEFF}, unmapped) // Nop writes, zero reads - mmu.AddHandler(mem.MemRegion{Start: 0xFF01, End: 0xFF02}, serial) // Serial Port (Control & Data) - mmu.AddHandler(mem.MemRegion{Start: 0xFF04, End: 0xFF07}, timer) // Timer (not RTC) - mmu.AddHandler(mem.MemRegion{Start: 0xFF40, End: 0xFF4B}, lcd) // LCD control registers + mmu.AddHandler(mem.MemRegion{Start: 0xFF01, End: 0xFF02}, dmg.serial) // Serial Port (Control & Data) + mmu.AddHandler(mem.MemRegion{Start: 0xFF04, End: 0xFF07}, dmg.timer) // Timer (not RTC) + mmu.AddHandler(mem.MemRegion{Start: 0xFF40, End: 0xFF4B}, dmg.lcd) // LCD control registers - mmu.AddHandler(mem.MemRegion{Start: 0xFF0F, End: 0xFF0F}, ic) + mmu.AddHandler(mem.MemRegion{Start: 0xFF0F, End: 0xFF0F}, dmg.ic) mmu.AddHandler(mem.MemRegion{Start: 0xFF4D, End: 0xFF77}, unmapped) // CGB regs - mmu.AddHandler(mem.MemRegion{Start: 0xFFFF, End: 0xFFFF}, ic) + mmu.AddHandler(mem.MemRegion{Start: 0xFFFF, End: 0xFFFF}, dmg.ic) - return &DMG{ - cpu: cpu, - mmu: mmu, - cartridge: cartridge, - debugger: debugger, - ic: ic, - host: host, - lcd: lcd, - serial: serial, - timer: timer, - }, nil + return dmg, nil +} + +func (dmg *DMG) AttachDebugger(debugger debug.Debugger) { + dmg.DetachDebugger() + + dmg.debuggerHandler = dmg.mmu.AddHandler(mem.MemRegion{Start: 0x0000, End: 0xFFFF}, debugger) + dmg.debugger = debugger +} + +func (dmg *DMG) DetachDebugger() { + // Remove any existing handlers + dmg.mmu.RemoveHandler(dmg.debuggerHandler) + dmg.debugger = debug.NewNullDebugger() } func (dmg *DMG) LoadCartridge(r *cart.Reader) error { @@ -86,27 +101,47 @@ func (dmg *DMG) DebugPrint(logger *log.Logger) { dmg.cartridge.DebugPrint(logger) } -func (dmg *DMG) Step() bool { +func (dmg *DMG) Step() error { dmg.debugger.OnDecode(dmg.cpu, dmg.mmu) cycles, err := dmg.cpu.Step(dmg.mmu) if err != nil { - dmg.host.LogErr("Unexpected error while executing instruction", err) - return false + return fmt.Errorf("Unexpected error while executing instruction: %w", err) } cycles += dmg.cpu.PollInterrupts(dmg.mmu, dmg.ic) - dmg.serial.Step(cycles, dmg.ic) dmg.timer.Step(cycles, dmg.ic) + dmg.serial.Step(cycles, dmg.ic) + + dmg.debugger.OnExecute(dmg.cpu, dmg.mmu) - return true + return nil } -func (dmg *DMG) Run() { +func (dmg *DMG) Run(host devices.HostInterface) error { + framebuffer := host.Framebuffer() + defer close(framebuffer) + + dmg.serial.AttachCable(host.SerialCable()) dmg.debugger.Setup(dmg.cpu, dmg.mmu) - for dmg.Step() { - dmg.debugger.OnExecute(dmg.cpu, dmg.mmu) + hostExit := host.Exited() + + fakeVBlank := time.NewTicker(time.Second / 60) + + for { + select { + case <-hostExit: + return nil + case <-fakeVBlank.C: + framebuffer <- dmg.lcd.Draw() + default: + // Do nothing + } + + if err := dmg.Step(); err != nil { + return err + } } } diff --git a/host/cli.go b/host/cli.go new file mode 100644 index 0000000..ee802c5 --- /dev/null +++ b/host/cli.go @@ -0,0 +1,83 @@ +package host + +import ( + "image" + "log" + + "github.com/maxfierke/gogo-gb/devices" + "github.com/maxfierke/gogo-gb/hardware" +) + +type CLIHost struct { + fbChan chan image.Image + logger *log.Logger + exitedChan chan bool + serialCable devices.SerialCable +} + +var _ Host = (*CLIHost)(nil) + +func NewCLIHost() *CLIHost { + return &CLIHost{ + fbChan: make(chan image.Image, 3), + exitedChan: make(chan bool), + logger: log.Default(), + serialCable: &devices.NullSerialCable{}, + } +} + +func (h *CLIHost) Framebuffer() chan<- image.Image { + return h.fbChan +} + +func (h *CLIHost) Log(msg string, args ...any) { + h.logger.Printf(msg+"\n", args...) +} + +func (h *CLIHost) LogErr(msg string, args ...any) { + h.Log("ERROR: "+msg, args...) +} + +func (h *CLIHost) LogWarn(msg string, args ...any) { + h.Log("WARN: "+msg, args...) +} + +func (h *CLIHost) SetLogger(logger *log.Logger) { + h.logger = logger +} + +func (h *CLIHost) Exited() <-chan bool { + return h.exitedChan +} + +func (h *CLIHost) SerialCable() devices.SerialCable { + return h.serialCable +} + +func (h *CLIHost) AttachSerialCable(serialCable devices.SerialCable) { + h.serialCable = serialCable +} + +func (h *CLIHost) Run(console hardware.Console) error { + done := make(chan error) + defer close(h.exitedChan) + + // "Renderer" + go func() { + for range h.fbChan { + // Consume frames + } + }() + + go func() { + if err := console.Run(h); err != nil { + h.LogErr("unexpected error occurred during runtime: %w", err) + done <- err + return + } + + done <- nil + }() + + return <-done +} diff --git a/host/host.go b/host/host.go new file mode 100644 index 0000000..6c46bdb --- /dev/null +++ b/host/host.go @@ -0,0 +1,16 @@ +package host + +import ( + "log" + + "github.com/maxfierke/gogo-gb/devices" + "github.com/maxfierke/gogo-gb/hardware" +) + +type Host interface { + devices.HostInterface + + AttachSerialCable(serialCable devices.SerialCable) + SetLogger(logger *log.Logger) + Run(console hardware.Console) error +} diff --git a/host/ui.go b/host/ui.go new file mode 100644 index 0000000..96e2679 --- /dev/null +++ b/host/ui.go @@ -0,0 +1,103 @@ +package host + +import ( + "errors" + "image" + "log" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/maxfierke/gogo-gb/devices" + "github.com/maxfierke/gogo-gb/hardware" +) + +type UI struct { + fbChan chan image.Image + logger *log.Logger + exitedChan chan bool + serialCable devices.SerialCable +} + +var _ Host = (*UI)(nil) + +func NewUIHost() *UI { + return &UI{ + fbChan: make(chan image.Image, 3), + exitedChan: make(chan bool), + logger: log.Default(), + serialCable: &devices.NullSerialCable{}, + } +} + +func (ui *UI) Framebuffer() chan<- image.Image { + return ui.fbChan +} + +func (ui *UI) Log(msg string, args ...any) { + ui.logger.Printf(msg+"\n", args...) +} + +func (ui *UI) LogErr(msg string, args ...any) { + ui.Log("ERROR: "+msg, args...) +} + +func (ui *UI) LogWarn(msg string, args ...any) { + ui.Log("WARN: "+msg, args...) +} + +func (ui *UI) Exited() <-chan bool { + return ui.exitedChan +} + +func (ui *UI) SetLogger(logger *log.Logger) { + ui.logger = logger +} + +func (ui *UI) SerialCable() devices.SerialCable { + return ui.serialCable +} + +func (ui *UI) AttachSerialCable(serialCable devices.SerialCable) { + ui.serialCable = serialCable +} + +func (ui *UI) Update() error { + return nil +} + +func (ui *UI) Draw(screen *ebiten.Image) { + select { + case fbImage := <-ui.fbChan: + image := ebiten.NewImageFromImage(fbImage) + screen.DrawImage(image, &ebiten.DrawImageOptions{}) + default: + // do nothing + } + ebitenutil.DebugPrint(screen, "gogo-gb!!!") +} + +func (ui *UI) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + return 160, 144 +} + +func (ui *UI) Run(console hardware.Console) error { + ebiten.SetWindowSize(640, 480) + ebiten.SetWindowTitle("gogo-gb, the go-getting GB emulator") + + if console == nil { + return errors.New("console cannot be nil") + } + + go func() { + ui.Log("Starting console main loop") + if err := console.Run(ui); err != nil { + ui.LogErr("unexpected error occurred during runtime: %w", err) + return + } + }() + + defer close(ui.exitedChan) + + ui.Log("Handing over to ebiten") + return ebiten.RunGame(ui) +} diff --git a/main.go b/main.go index dfc5241..0b7ebbb 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "log" "os" @@ -10,6 +11,7 @@ import ( "github.com/maxfierke/gogo-gb/debug" "github.com/maxfierke/gogo-gb/devices" "github.com/maxfierke/gogo-gb/hardware" + "github.com/maxfierke/gogo-gb/host" ) type CLIOptions struct { @@ -19,6 +21,7 @@ type CLIOptions struct { logPath string logger *log.Logger serialPort string + ui bool } const LOG_PREFIX = "" @@ -45,8 +48,10 @@ func main() { if options.debugPrint != "" { debugPrint(&options) } else { - options.logger.Println("welcome to gogo-gb, the go-getting gameboy emulator") - runCart(&options) + options.logger.Println("welcome to gogo-gb, the go-getting GB emulator") + if err := runCart(&options); err != nil { + options.logger.Fatalf("ERROR: %v\n", err) + } } } @@ -56,6 +61,7 @@ func parseOptions(options *CLIOptions) { flag.StringVar(&options.debugger, "debugger", "none", "Specify debugger to use (\"none\", \"gameboy-doctor\")") flag.StringVar(&options.debugPrint, "debug-print", "", "Print out something for debugging purposes (\"cart-header\", \"opcodes\")") flag.StringVar(&options.logPath, "log", "", "Path to log file. Default/empty implies stdout") + flag.BoolVar(&options.ui, "ui", false, "Launch with UI") flag.Parse() } @@ -100,11 +106,16 @@ func debugPrintOpcodes(options *CLIOptions) { opcodes.DebugPrint(logger) } -func initDMG(options *CLIOptions) *hardware.DMG { - logger := options.logger +func initHost(options *CLIOptions) (host.Host, error) { + var hostDevice host.Host + + if options.ui { + hostDevice = host.NewUIHost() + } else { + hostDevice = host.NewCLIHost() + } - host := devices.NewHost() - host.SetLogger(logger) + hostDevice.SetLogger(options.logger) if options.serialPort != "" { serialCable := devices.NewHostSerialCable() @@ -116,39 +127,45 @@ func initDMG(options *CLIOptions) *hardware.DMG { } else { serialPort, err := os.Create(options.serialPort) if err != nil { - logger.Fatalf("ERROR: Unable to open file '%s' as serial port: %v\n", options.serialPort, err) + return nil, fmt.Errorf("unable to open file '%s' as serial port: %w", options.serialPort, err) } serialCable.SetReader(serialPort) serialCable.SetWriter(serialPort) } - host.AttachSerialCable(serialCable) + hostDevice.AttachSerialCable(serialCable) } + return hostDevice, nil +} + +func initDMG(options *CLIOptions) (*hardware.DMG, error) { debugger, err := debug.NewDebugger(options.debugger) if err != nil { - logger.Fatalf("ERROR: Unable to initialize Debugger: %v\n", err) + return nil, fmt.Errorf("unable to initialize Debugger: %w", err) } - dmg, err := hardware.NewDMGDebug(host, debugger) + dmg, err := hardware.NewDMG( + hardware.WithDebugger(debugger), + ) if err != nil { - logger.Fatalf("ERROR: Unable to initialize DMG: %v\n", err) + return nil, fmt.Errorf("unable to initialize DMG: %w", err) } - return dmg + return dmg, nil } -func loadCart(dmg *hardware.DMG, options *CLIOptions) { +func loadCart(dmg *hardware.DMG, options *CLIOptions) error { if options.cartPath == "" { - return + return nil } logger := options.logger cartFile, err := os.Open(options.cartPath) if options.cartPath == "" || err != nil { - logger.Fatalf("ERROR: Unable to load cartridge. Please ensure it's inserted correctly (e.g. file exists): %v\n", err) + return fmt.Errorf("unable to load cartridge. Please ensure it's inserted correctly (e.g. file exists): %w", err) } defer cartFile.Close() @@ -156,19 +173,38 @@ func loadCart(dmg *hardware.DMG, options *CLIOptions) { if err == cart.ErrHeader { logger.Printf("WARN: Cartridge header does not match expected checksum. Continuing, but subsequent operations may fail") } else if err != nil { - logger.Fatalf("ERROR: Unable to load cartridge. Please ensure it's inserted correctly (e.g. file exists): %v\n", err) + return fmt.Errorf("unable to load cartridge. Please ensure it's inserted correctly (e.g. file exists): %w", err) } err = dmg.LoadCartridge(cartReader) if err == cart.ErrHeader { logger.Printf("WARN: Cartridge header does not match expected checksum. Continuing, but subsequent operations may fail") } else if err != nil { - logger.Fatalf("ERROR: Unable to load cartridge: %v\n", err) + return fmt.Errorf("unable to load cartridge: %w", err) } + + return nil } -func runCart(options *CLIOptions) { - dmg := initDMG(options) - loadCart(dmg, options) - dmg.Run() +func runCart(options *CLIOptions) error { + hostDevice, err := initHost(options) + if err != nil { + return fmt.Errorf("unable to initialize host device: %w", err) + } + + dmg, err := initDMG(options) + if err != nil { + return err + } + + if err := loadCart(dmg, options); err != nil { + return err + } + + err = hostDevice.Run(dmg) + if err != nil { + return err + } + + return nil }