diff --git a/README.md b/README.md index 4a841fc..5db452a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # gogo-gb a gameboy emulator for funsies +Current status: Games are playable, but slow. Graphics are buggy. No audio. + ## TODO - [X] Pass all of Blargg's `cpu_instrs` ROMs via `gameboy-doctor` (expect `02-interrupts.gb`, which isn't verifyable via `gameboy-doctor`) @@ -15,9 +17,10 @@ a gameboy emulator for funsies - [ ] Pass all of Blargg's `mem_timing-2` ROMs (manually verified) - [X] Implement Joypad - [ ] Implement RTC for MBC3 -- [ ] Implement SRAM save & restore +- [X] Implement SRAM save & restore - [ ] Pass `dmg-acid2` test ROM - [ ] Implement Sound/APU +- [ ] Implement GBC ## Maybe Never? @@ -26,6 +29,7 @@ Just being realistic about my likelihood of getting to these: - [ ] FIFO-based rendering PPU (currently scanline) - [ ] Implement emulation for every known DMG bug - [ ] Implement SGB mode +- [ ] Implement MBC2 - [ ] Implement MBC6 - [ ] Implement MBC7 - [ ] Implement any multicarts or Hudson carts diff --git a/cart/cartridge.go b/cart/cartridge.go index 797e36e..0116fc7 100644 --- a/cart/cartridge.go +++ b/cart/cartridge.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "io" "log" "github.com/maxfierke/gogo-gb/cart/mbc" @@ -17,7 +18,7 @@ var ( type Cartridge struct { Header Header - mbc mem.MemHandler + mbc mbc.MBC } func NewCartridge() *Cartridge { @@ -81,6 +82,14 @@ func (c *Cartridge) LoadCartridge(r *Reader) error { return nil } +func (c *Cartridge) Save(w io.Writer) error { + return c.mbc.Save(w) +} + +func (c *Cartridge) LoadSave(r io.Reader) error { + return c.mbc.LoadSave(r) +} + func (c *Cartridge) OnRead(mmu *mem.MMU, addr uint16) mem.MemRead { if c.mbc == nil { return mem.ReadPassthrough() diff --git a/cart/header.go b/cart/header.go index 5e356c2..134e67a 100644 --- a/cart/header.go +++ b/cart/header.go @@ -238,6 +238,10 @@ func (hdr *Header) RamSizeBytes() uint { } } +func (hdr *Header) SupportsSaving() bool { + return hdr.RamSizeBytes() > 0 +} + func (hdr *Header) DebugPrint(logger *log.Logger) { logger.Printf("== Cartridge Info ==\n") logger.Printf("\n") diff --git a/cart/mbc/common.go b/cart/mbc/common.go index beceda6..3cd4a80 100644 --- a/cart/mbc/common.go +++ b/cart/mbc/common.go @@ -1,6 +1,10 @@ package mbc -import "github.com/maxfierke/gogo-gb/mem" +import ( + "io" + + "github.com/maxfierke/gogo-gb/mem" +) const ( RAM_BANK_SIZE = 0x2000 @@ -18,3 +22,9 @@ func writeBankAddr(memory []byte, banksRegion mem.MemRegion, bankSize uint16, cu bankSlotAddr := uint(addr) - uint(banksRegion.Start) memory[bankBaseAddr+bankSlotAddr] = value } + +type MBC interface { + mem.MemHandler + Save(w io.Writer) error + LoadSave(r io.Reader) error +} diff --git a/cart/mbc/mbc0.go b/cart/mbc/mbc0.go index 845aa9e..7b48c86 100644 --- a/cart/mbc/mbc0.go +++ b/cart/mbc/mbc0.go @@ -2,6 +2,7 @@ package mbc import ( "fmt" + "io" "github.com/maxfierke/gogo-gb/mem" ) @@ -15,6 +16,8 @@ type MBC0 struct { rom []byte } +var _ MBC = (*MBC0)(nil) + func NewMBC0(rom []byte) *MBC0 { return &MBC0{rom: rom} } @@ -40,3 +43,11 @@ func (m *MBC0) OnWrite(mmu *mem.MMU, addr uint16, value byte) mem.MemWrite { panic(fmt.Sprintf("Attempting to write 0x%02X @ 0x%04X, which is out-of-bounds for MBC0", value, addr)) } + +func (m *MBC0) Save(w io.Writer) error { + return nil +} + +func (m *MBC0) LoadSave(r io.Reader) error { + return nil +} diff --git a/cart/mbc/mbc1.go b/cart/mbc/mbc1.go index 195a54a..b65de9a 100644 --- a/cart/mbc/mbc1.go +++ b/cart/mbc/mbc1.go @@ -2,6 +2,7 @@ package mbc import ( "fmt" + "io" "github.com/maxfierke/gogo-gb/mem" ) @@ -34,6 +35,8 @@ type MBC1 struct { rom []byte } +var _ MBC = (*MBC1)(nil) + func NewMBC1(rom []byte, ram []byte) *MBC1 { return &MBC1{ curRamBank: 0, @@ -128,3 +131,29 @@ func (m *MBC1) OnWrite(mmu *mem.MMU, addr uint16, value byte) mem.MemWrite { panic(fmt.Sprintf("Attempting to write 0x%02X @ 0x%04X, which is out-of-bounds for MBC1", value, addr)) } + +func (m *MBC1) Save(w io.Writer) error { + if len(m.ram) == 0 { + return nil + } + + n, err := w.Write(m.ram) + if err != nil { + return fmt.Errorf("mbc1: saving SRAM: %w. wrote %d bytes", err, n) + } + + return nil +} + +func (m *MBC1) LoadSave(r io.Reader) error { + if len(m.ram) == 0 { + return nil + } + + n, err := io.ReadFull(r, m.ram) + if err != nil { + return fmt.Errorf("mbc1: loading save into SRAM: %w. read %d bytes", err, n) + } + + return nil +} diff --git a/cart/mbc/mbc3.go b/cart/mbc/mbc3.go index 57761ec..769aee5 100644 --- a/cart/mbc/mbc3.go +++ b/cart/mbc/mbc3.go @@ -2,6 +2,7 @@ package mbc import ( "fmt" + "io" "github.com/maxfierke/gogo-gb/mem" ) @@ -60,6 +61,8 @@ type MBC3 struct { rtcDaysOverflow bool } +var _ MBC = (*MBC3)(nil) + func NewMBC3(rom []byte, ram []byte, rtcAvailable bool) *MBC3 { return &MBC3{ ram: ram, @@ -210,6 +213,36 @@ func (m *MBC3) writeRtcReg(reg mbc3RtcReg, value byte) { } } +func (m *MBC3) Save(w io.Writer) error { + if len(m.ram) == 0 { + return nil + } + + n, err := w.Write(m.ram) + if err != nil { + return fmt.Errorf("mbc3: saving SRAM: %w. wrote %d bytes", err, n) + } + + // TODO: Write RTC registers + + return nil +} + +func (m *MBC3) LoadSave(r io.Reader) error { + if len(m.ram) == 0 { + return nil + } + + n, err := io.ReadFull(r, m.ram) + if err != nil { + return fmt.Errorf("mbc3: loading save into SRAM: %w. read %d bytes", err, n) + } + + // TODO: Read RTC registers + + return nil +} + var ( MBC30_ROM_BANKS = mem.MemRegion{Start: 0x4000, End: 0x7FFF} @@ -269,3 +302,11 @@ func (m *MBC30) OnWrite(mmu *mem.MMU, addr uint16, value byte) mem.MemWrite { return m.MBC3.OnWrite(mmu, addr, value) } + +func (m *MBC30) Save(w io.Writer) error { + return m.MBC3.Save(w) +} + +func (m *MBC30) LoadSave(r io.Reader) error { + return m.MBC3.LoadSave(r) +} diff --git a/cart/mbc/mbc5.go b/cart/mbc/mbc5.go index 4fa662f..4c127e4 100644 --- a/cart/mbc/mbc5.go +++ b/cart/mbc/mbc5.go @@ -2,6 +2,7 @@ package mbc import ( "fmt" + "io" "github.com/maxfierke/gogo-gb/mem" ) @@ -34,6 +35,8 @@ type MBC5 struct { rom []byte } +var _ MBC = (*MBC5)(nil) + func NewMBC5(rom []byte, ram []byte) *MBC5 { return &MBC5{ curRamBank: 0, @@ -114,3 +117,29 @@ func (m *MBC5) OnWrite(mmu *mem.MMU, addr uint16, value byte) mem.MemWrite { panic(fmt.Sprintf("Attempting to write 0x%02X @ 0x%04X, which is out-of-bounds for MBC5", value, addr)) } + +func (m *MBC5) Save(w io.Writer) error { + if len(m.ram) == 0 { + return nil + } + + n, err := w.Write(m.ram) + if err != nil { + return fmt.Errorf("mbc5: saving SRAM: %w. wrote %d bytes", err, n) + } + + return nil +} + +func (m *MBC5) LoadSave(r io.Reader) error { + if len(m.ram) == 0 { + return nil + } + + n, err := io.ReadFull(r, m.ram) + if err != nil { + return fmt.Errorf("mbc5: loading save into SRAM: %w. read %d bytes", err, n) + } + + return nil +} diff --git a/hardware/console.go b/hardware/console.go index 1147abf..1e23a57 100644 --- a/hardware/console.go +++ b/hardware/console.go @@ -1,6 +1,8 @@ package hardware import ( + "io" + "github.com/maxfierke/gogo-gb/cart" "github.com/maxfierke/gogo-gb/debug" "github.com/maxfierke/gogo-gb/devices" @@ -9,7 +11,10 @@ import ( type Console interface { AttachDebugger(debugger debug.Debugger) DetachDebugger() + CartridgeHeader() cart.Header LoadCartridge(r *cart.Reader) error + Save(w io.Writer) error + LoadSave(r io.Reader) error Step() error Run(host devices.HostInterface) error } diff --git a/hardware/dmg.go b/hardware/dmg.go index 66fd0f4..e1e207c 100644 --- a/hardware/dmg.go +++ b/hardware/dmg.go @@ -138,8 +138,39 @@ func (dmg *DMG) DetachDebugger() { dmg.debugger = debug.NewNullDebugger() } +func (dmg *DMG) CartridgeHeader() cart.Header { + if dmg.cartridge == nil { + return cart.Header{} + } + + return dmg.cartridge.Header +} + func (dmg *DMG) LoadCartridge(r *cart.Reader) error { - return dmg.cartridge.LoadCartridge(r) + err := dmg.cartridge.LoadCartridge(r) + if err != nil { + return fmt.Errorf("dmg: loading cartridge: %w", err) + } + + return nil +} + +func (dmg *DMG) LoadSave(r io.Reader) error { + err := dmg.cartridge.LoadSave(r) + if err != nil { + return fmt.Errorf("dmg: loading save: %w", err) + } + + return nil +} + +func (dmg *DMG) Save(w io.Writer) error { + err := dmg.cartridge.Save(w) + if err != nil { + return fmt.Errorf("dmg: writing save: %w", err) + } + + return nil } func (dmg *DMG) DebugPrint(logger *log.Logger) { diff --git a/main.go b/main.go index bed822d..b2d7a7e 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,8 @@ import ( "fmt" "log" "os" + "path/filepath" + "strings" "github.com/maxfierke/gogo-gb/cart" "github.com/maxfierke/gogo-gb/cpu/isa" @@ -16,15 +18,16 @@ import ( ) type CLIOptions struct { - bootRomPath string - cartPath string - debugger string - debugPrint string - logPath string - logger *log.Logger - serialPort string - skipBootRom bool - ui bool + bootRomPath string + cartPath string + cartSavePath string + debugger string + debugPrint string + logPath string + logger *log.Logger + serialPort string + skipBootRom bool + ui bool } const LOG_PREFIX = "" @@ -68,6 +71,7 @@ func main() { func parseOptions(options *CLIOptions) { flag.StringVar(&options.bootRomPath, "bootrom", "", "Path to boot ROM file (dmg_bios.bin, mgb_bios.bin, etc.). Defaults to a lookup on common boot ROM filenames in current directory") flag.StringVar(&options.cartPath, "cart", "", "Path to cartridge file (.gb, .gbc)") + flag.StringVar(&options.cartSavePath, "cart-save", "", "Path to cartridge save file (.sav). Defaults to a .sav file with the same name as the cartridge file") flag.StringVar(&options.debugger, "debugger", "none", "Specify debugger to use (\"none\", \"gameboy-doctor\", \"interactive\")") 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") @@ -118,6 +122,24 @@ func debugPrintOpcodes(options *CLIOptions) { opcodes.DebugPrint(logger) } +func getCartSaveFilePath(options *CLIOptions) string { + cartSaveFilePath := options.cartSavePath + + if cartSaveFilePath == "" && options.cartPath != "" { + cartSaveDir := filepath.Dir(options.cartPath) + cartSaveFileName := strings.Replace( + filepath.Base(options.cartPath), + filepath.Ext(options.cartPath), + ".sav", + 1, + ) + + cartSaveFilePath = filepath.Join(cartSaveDir, cartSaveFileName) + } + + return cartSaveFilePath +} + func initHost(options *CLIOptions) (host.Host, error) { var hostDevice host.Host @@ -216,16 +238,16 @@ func loadBootROM(options *CLIOptions) (*os.File, error) { return bootRomFile, nil } -func loadCart(dmg *hardware.DMG, options *CLIOptions) error { +func loadCart(dmg *hardware.DMG, options *CLIOptions) (*cart.Reader, error) { if options.cartPath == "" { - return nil + return nil, nil } logger := options.logger cartFile, err := os.Open(options.cartPath) if options.cartPath == "" || err != nil { - return fmt.Errorf("unable to load cartridge. Please ensure it's inserted correctly (e.g. file exists): %w", err) + return nil, fmt.Errorf("unable to load cartridge. Please ensure it's inserted correctly (e.g. file exists): %w", err) } defer cartFile.Close() @@ -233,7 +255,7 @@ func loadCart(dmg *hardware.DMG, options *CLIOptions) error { if errors.Is(err, cart.ErrHeader) { logger.Printf("WARN: Cartridge header does not match expected checksum. Continuing, but subsequent operations may fail") } else if err != nil { - return fmt.Errorf("unable to load cartridge. Please ensure it's inserted correctly (e.g. file exists): %w", err) + return nil, fmt.Errorf("unable to load cartridge. Please ensure it's inserted correctly (e.g. file exists): %w", err) } cartReader.Header.DebugPrint(logger) @@ -242,12 +264,53 @@ func loadCart(dmg *hardware.DMG, options *CLIOptions) error { if errors.Is(err, cart.ErrHeader) { logger.Printf("WARN: Cartridge header does not match expected checksum. Continuing, but subsequent operations may fail") } else if err != nil { - return fmt.Errorf("unable to load cartridge: %w", err) + return nil, fmt.Errorf("unable to load cartridge: %w", err) + } + + return cartReader, nil +} + +func loadCartSave(dmg *hardware.DMG, options *CLIOptions) error { + cartSaveFilePath := getCartSaveFilePath(options) + + cartSaveFile, err := os.Open(cartSaveFilePath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("unable to open or create cartridge save file: %w", err) + } + + if cartSaveFile != nil { + defer cartSaveFile.Close() + + err = dmg.LoadSave(cartSaveFile) + if err != nil { + return fmt.Errorf("unable to load cartridge save: %w", err) + } + + options.logger.Printf("Loaded cartridge save from %s\n", cartSaveFilePath) } return nil } +func saveCart(dmg *hardware.DMG, options *CLIOptions) error { + cartSaveFilePath := getCartSaveFilePath(options) + + cartSaveFile, err := os.OpenFile(cartSaveFilePath, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("unable to open or create cartridge save file: %w", err) + } + defer cartSaveFile.Close() + + err = dmg.Save(cartSaveFile) + if err != nil { + return fmt.Errorf("unable to write cartridge save file: %w", err) + } + + options.logger.Printf("Saved cartridge save to %s\n", cartSaveFilePath) + + return nil +} + func runCart(options *CLIOptions) error { hostDevice, err := initHost(options) if err != nil { @@ -256,16 +319,31 @@ func runCart(options *CLIOptions) error { dmg, err := initDMG(options) if err != nil { - return err + return fmt.Errorf("initializing DMG: %w", err) } - if err := loadCart(dmg, options); err != nil { - return err + cartReader, err := loadCart(dmg, options) + if err != nil { + return fmt.Errorf("loading cartridge: %w", err) + } + + if cartReader.Header.SupportsSaving() { + err := loadCartSave(dmg, options) + if err != nil { + return fmt.Errorf("loading cartridge save: %w", err) + } + + defer func() { + err := saveCart(dmg, options) + if err != nil { + options.logger.Printf("WARN: Error occurred while saving: %s", err.Error()) + } + }() } err = hostDevice.Run(dmg) if err != nil { - return err + return fmt.Errorf("running emulation: %w", err) } return nil