diff --git a/.gitignore b/.gitignore index c30a90f..bdaacd4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .vscode/ # Binaries for programs and plugins +*.bin *.exe *.exe~ *.dll diff --git a/Makefile b/Makefile index 8a8e7a7..10e54cb 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,7 @@ cpu_instrs: bin/gogo-gb tests/gameboy-doctor/gameboy-doctor tests/gb-test-roms/c test_num=$$((10#$${test_name%-*})); \ echo "=== Starting cpu_instrs test $$file ==="; \ bin/gogo-gb --cart "tests/gb-test-roms/cpu_instrs/individual/$$file" \ + --skip-bootrom \ --debugger=gameboy-doctor \ --log=stderr | \ ./tests/gameboy-doctor/gameboy-doctor - cpu_instrs "$$test_num" || \ diff --git a/devices/boot_rom.go b/devices/boot_rom.go new file mode 100644 index 0000000..186170b --- /dev/null +++ b/devices/boot_rom.go @@ -0,0 +1,46 @@ +package devices + +import ( + "fmt" + "log" + + "github.com/maxfierke/gogo-gb/mem" +) + +const ( + REG_BOOTROM_EN = 0xFF50 +) + +type BootROM struct { + enabled bool + rom []byte +} + +func NewBootROM() *BootROM { + return &BootROM{enabled: false, rom: []byte{}} +} + +func (br *BootROM) LoadROM(rom []byte) { + br.enabled = true + br.rom = rom +} + +func (br *BootROM) OnRead(mmu *mem.MMU, addr uint16) mem.MemRead { + if br.enabled { + return mem.ReadReplace(br.rom[addr]) + } else { + return mem.ReadPassthrough() + } +} + +func (br *BootROM) OnWrite(mmu *mem.MMU, addr uint16, value byte) mem.MemWrite { + if addr == REG_BOOTROM_EN && br.enabled { + br.enabled = value == 0x00 + log.Printf("Unloaded boot ROM") + return mem.WriteBlock() + } else if br.enabled { + panic(fmt.Sprintf("Attempting to write 0x%02X @ 0x%04X, which is not allowed for boot ROM", value, addr)) + } else { + return mem.WritePassthrough() + } +} diff --git a/hardware/dmg.go b/hardware/dmg.go index 2aa1e1b..32cff8e 100644 --- a/hardware/dmg.go +++ b/hardware/dmg.go @@ -2,6 +2,7 @@ package hardware import ( "fmt" + "io" "log" "time" @@ -13,15 +14,40 @@ import ( ) const ( - DMG_CPU_HZ = 4194304 - DMG_RAM_SIZE = 0xFFFF + 1 + DMG_BOOTROM_SIZE = 0x100 + DMG_CPU_HZ = 4194304 + DMG_RAM_SIZE = 0x10000 ) -type DMGOption func(dmg *DMG) +type DMGOption func(dmg *DMG) error + +func WithBootROM(r io.Reader) DMGOption { + return func(dmg *DMG) error { + rom := make([]byte, DMG_BOOTROM_SIZE) + if _, err := r.Read(rom); err != nil { + return fmt.Errorf("unable to load boot ROM: %w", err) + } + + dmg.bootROM = devices.NewBootROM() + dmg.bootROM.LoadROM(rom) + + dmg.mmu.AddHandler(mem.MemRegion{Start: 0x0000, End: 0x00FF}, dmg.bootROM) + + return nil + } +} func WithDebugger(debugger debug.Debugger) DMGOption { - return func(dmg *DMG) { + return func(dmg *DMG) error { dmg.AttachDebugger(debugger) + return nil + } +} + +func WithFakeBootROM() DMGOption { + return func(dmg *DMG) error { + dmg.cpu.ResetToBootROM() + return nil } } @@ -36,6 +62,7 @@ type DMG struct { timer *devices.Timer // Non-components + bootROM *devices.BootROM debugger debug.Debugger debuggerHandler mem.MemHandlerHandle } @@ -63,7 +90,10 @@ func NewDMG(opts ...DMGOption) (*DMG, error) { } for _, opt := range opts { - opt(dmg) + err = opt(dmg) + if err != nil { + return nil, err + } } mmu.AddHandler(mem.MemRegion{Start: 0x0000, End: 0x7FFF}, dmg.cartridge) // MBCs ROM Banks diff --git a/main.go b/main.go index 42f116c..3386067 100644 --- a/main.go +++ b/main.go @@ -16,17 +16,26 @@ import ( ) type CLIOptions struct { - cartPath string - debugger string - debugPrint string - logPath string - logger *log.Logger - serialPort string - ui bool + bootRomPath string + cartPath string + debugger string + debugPrint string + logPath string + logger *log.Logger + serialPort string + skipBootRom bool + ui bool } const LOG_PREFIX = "" +var DEFAULT_BOOT_ROM_PATHS = []string{ + "gb_bios.bin", + "dmg_bios.bin", + "mgb_bios.bin", + "dmg0_bios.bin", +} + func main() { options := CLIOptions{} @@ -57,11 +66,13 @@ 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.serialPort, "serial-port", "", "Path to serial port IO (could be a file, UNIX socket, etc.)") 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") + flag.StringVar(&options.serialPort, "serial-port", "", "Path to serial port IO (could be a file, UNIX socket, etc.)") + flag.BoolVar(&options.skipBootRom, "skip-bootrom", false, "Skip loading a boot ROM") flag.BoolVar(&options.ui, "ui", false, "Launch with UI") flag.Parse() } @@ -147,9 +158,26 @@ func initDMG(options *CLIOptions) (*hardware.DMG, error) { return nil, fmt.Errorf("unable to initialize Debugger: %w", err) } - dmg, err := hardware.NewDMG( + opts := []hardware.DMGOption{ hardware.WithDebugger(debugger), - ) + } + + if options.skipBootRom { + opts = append(opts, hardware.WithFakeBootROM()) + } else { + bootRomFile, err := loadBootROM(options) + if err != nil { + return nil, fmt.Errorf("unable to load boot ROM: %w", err) + } + if bootRomFile == nil { + opts = append(opts, hardware.WithFakeBootROM()) + } else { + defer bootRomFile.Close() + opts = append(opts, hardware.WithBootROM(bootRomFile)) + } + } + + dmg, err := hardware.NewDMG(opts...) if err != nil { return nil, fmt.Errorf("unable to initialize DMG: %w", err) } @@ -157,6 +185,37 @@ func initDMG(options *CLIOptions) (*hardware.DMG, error) { return dmg, nil } +func loadBootROM(options *CLIOptions) (*os.File, error) { + logger := options.logger + bootRomPath := options.bootRomPath + + var bootRomFile *os.File + var err error + + if bootRomPath == "" { + for _, romPath := range DEFAULT_BOOT_ROM_PATHS { + if bootRomFile, err = os.Open(romPath); err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, err + } else if bootRomFile != nil { + // yay! we found one! + break + } + } + } else if bootRomFile, err = os.Open(bootRomPath); err != nil { + return nil, err + } + + if bootRomFile == nil { + // Bail out if no boot ROM loaded + logger.Printf("WARN: No boot ROM provided. Some emulation functionality may be incorrect.") + return nil, nil + } + + logger.Printf("Loaded boot ROM: %s\n", bootRomFile.Name()) + + return bootRomFile, nil +} + func loadCart(dmg *hardware.DMG, options *CLIOptions) error { if options.cartPath == "" { return nil