diff --git a/gb/cartridge.go b/gb/cartridge.go new file mode 100644 index 0000000..f13c48d --- /dev/null +++ b/gb/cartridge.go @@ -0,0 +1,301 @@ +package gb + +import ( + "bytes" + "encoding/binary" + "io" + "log" + "os" +) + +// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0104-0133--nintendo-logo +var expectedLogo = [48]byte{ + 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, + 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, + 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, + 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, + 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, + 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, +} + +// Cartridge types +// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0147--cartridge-type +var cartridgeTypes = map[byte]string{ + 0x00: "ROM ONLY", + 0x01: "MBC1", + 0x02: "MBC1+RAM", + 0x03: "MBC1+RAM+BATTERY", + 0x05: "MBC2", + 0x06: "MBC2+BATTERY", + 0x08: "ROM+RAM 9", + 0x09: "ROM+RAM+BATTERY 9", + 0x0B: "MMM01", + 0x0C: "MMM01+RAM", + 0x0D: "MMM01+RAM+BATTERY", + 0x0F: "MBC3+TIMER+BATTERY", + 0x10: "MBC3+TIMER+RAM+BATTERY 10", + 0x11: "MBC3", + 0x12: "MBC3+RAM 10", + 0x13: "MBC3+RAM+BATTERY 10", + 0x19: "MBC5", + 0x1A: "MBC5+RAM", + 0x1B: "MBC5+RAM+BATTERY", + 0x1C: "MBC5+RUMBLE", + 0x1D: "MBC5+RUMBLE+RAM", + 0x1E: "MBC5+RUMBLE+RAM+BATTERY", + 0x20: "MBC6", + 0x22: "MBC7+SENSOR+RUMBLE+RAM+BATTERY", + 0xFC: "POCKET CAMERA", + 0xFD: "BANDAI TAMA5", + 0xFE: "HuC3", + 0xFF: "HuC1+RAM+BATTERY", +} + +// RAM sizes +// https://gbdev.io/pandocs/The_Cartridge_Header.html#0149--ram-size +var ramSizes = map[byte]string{ + 0x00: "0 - No RAM", + 0x01: "UNUSED VALUE", + 0x02: "8 KiB - 1 bank", + 0x03: "32 KiB - 4 banks of 8 KiB each", + 0x04: "128 KiB 16 banks of 8 KiB each", + 0x05: "64 KiB 8 banks of 8 KiB each", +} + +// Old licensees +// https://gbdev.io/pandocs/The_Cartridge_Header.html#014b--old-licensee-code +var oldLicensees = map[byte]string{ + 0x00: "None", + 0x01: "Nintendo", + 0x08: "Capcom", + 0x09: "HOT-B", + 0x0A: "Jaleco", + 0x0B: "Coconuts Japan", + 0x0C: "Elite Systems", + 0x13: "EA (Electronic Arts)", + 0x18: "Hudson Soft", + 0x19: "ITC Entertainment", + 0x1A: "Yanoman", + 0x1D: "Japan Clary", + 0x1F: "Virgin Games Ltd.3", + 0x24: "PCM Complete", + 0x25: "San-X", + 0x28: "Kemco", + 0x29: "SETA Corporation", + 0x30: "Infogrames5", + 0x31: "Nintendo", + 0x32: "Bandai", + 0x33: "Indicates that the New licensee code should be used instead.", + 0x34: "Konami", + 0x35: "HectorSoft", + 0x38: "Capcom", + 0x39: "Banpresto", + 0x3C: "Entertainment Interactive (stub)", + 0x3E: "Gremlin", + 0x41: "Ubi Soft1", + 0x42: "Atlus", + 0x44: "Malibu Interactive", + 0x46: "Angel", + 0x47: "Spectrum HoloByte", + 0x49: "Irem", + 0x4A: "Virgin Games Ltd.3", + 0x4D: "Malibu Interactive", + 0x4F: "U.S. Gold", + 0x50: "Absolute", + 0x51: "Acclaim Entertainment", + 0x52: "Activision", + 0x53: "Sammy USA Corporation", + 0x54: "GameTek", + 0x55: "Park Place13", + 0x56: "LJN", + 0x57: "Matchbox", + 0x59: "Milton Bradley Company", + 0x5A: "Mindscape", + 0x5B: "Romstar", + 0x5C: "Naxat Soft14", + 0x5D: "Tradewest", + 0x60: "Titus Interactive", + 0x61: "Virgin Games Ltd.3", + 0x67: "Ocean Software", + 0x69: "EA (Electronic Arts)", + 0x6E: "Elite Systems", + 0x6F: "Electro Brain", + 0x70: "Infogrames5", + 0x71: "Interplay Entertainment", + 0x72: "Broderbund", + 0x73: "Sculptured Software6", + 0x75: "The Sales Curve Limited7", + 0x78: "THQ", + 0x79: "Accolade15", + 0x7A: "Triffix Entertainment", + 0x7C: "MicroProse", + 0x7F: "Kemco", + 0x80: "Misawa Entertainment", + 0x83: "LOZC G.", + 0x86: "Tokuma Shoten", + 0x8B: "Bullet-Proof Software2", + 0x8C: "Vic Tokai Corp.16", + 0x8E: "Ape Inc.17", + 0x8F: "I'Max18", + 0x91: "Chunsoft Co.8", + 0x92: "Video System", + 0x93: "Tsubaraya Productions", + 0x95: "Varie", + 0x96: "Yonezawa19/S'Pal", + 0x97: "Kemco", + 0x99: "Arc", + 0x9A: "Nihon Bussan", + 0x9B: "Tecmo", + 0x9C: "Imagineer", + 0x9D: "Banpresto", + 0x9F: "Nova", + 0xA1: "Hori Electric", + 0xA2: "Bandai", + 0xA4: "Konami", + 0xA6: "Kawada", + 0xA7: "Takara", + 0xA9: "Technos Japan", + 0xAA: "Broderbund", + 0xAC: "Toei Animation", + 0xAD: "Toho", + 0xAF: "Namco", + 0xB0: "Acclaim Entertainment", + 0xB1: "ASCII Corporation or Nexsoft", + 0xB2: "Bandai", + 0xB4: "Square Enix", + 0xB6: "HAL Laboratory", + 0xB7: "SNK", + 0xB9: "Pony Canyon", + 0xBA: "Culture Brain", + 0xBB: "Sunsoft", + 0xBD: "Sony Imagesoft", + 0xBF: "Sammy Corporation", + 0xC0: "Taito", + 0xC2: "Kemco", + 0xC3: "Square", + 0xC4: "Tokuma Shoten", + 0xC5: "Data East", + 0xC6: "Tonkin House", + 0xC8: "Koei", + 0xC9: "UFL", + 0xCA: "Ultra Games", + 0xCB: "VAP, Inc.", + 0xCC: "Use Corporation", + 0xCD: "Meldac", + 0xCE: "Pony Canyon", + 0xCF: "Angel", + 0xD0: "Taito", + 0xD1: "SOFEL (Software Engineering Lab)", + 0xD2: "Quest", + 0xD3: "Sigma Enterprises", + 0xD4: "ASK Kodansha Co.", + 0xD6: "Naxat Soft14", + 0xD7: "Copya System", + 0xD9: "Banpresto", + 0xDA: "Tomy", + 0xDB: "LJN", + 0xDD: "Nippon Computer Systems", + 0xDE: "Human Ent.", + 0xDF: "Altron", + 0xE0: "Jaleco", + 0xE1: "Towa Chiki", + 0xE2: "Yutaka # Needs more info", + 0xE3: "Varie", + 0xE5: "Epoch", + 0xE7: "Athena", + 0xE8: "Asmik Ace Entertainment", + 0xE9: "Natsume", + 0xEA: "King Records", + 0xEB: "Atlus", + 0xEC: "Epic/Sony Records", + 0xEE: "IGS", + 0xF0: "A Wave", + 0xF3: "Extreme Entertainment", + 0xFF: "LJN", +} + +type ROMHeader struct { + EntryPoint [4]byte + Logo [48]byte + // NOTE(m): Assuming "old" cartridges here. This may cause problems with newer cartridges. + // See https://gbdev.io/pandocs/The_Cartridge_Header.html#0134-0143--title + Title [16]byte + NewLicenseeCode [2]byte + SGBFlag byte + CartridgeType byte + ROMSize byte + RAMSize byte + DestinationCode byte + OldLicenseeCode byte + MaskROMVersionNumber byte + HeaderChecksum byte + GlobalChecksum [2]byte +} + +type Cartridge struct { + Filename string + Mapper string + Title string + Licensee string + SGBSupport bool + ROMSize int + RAMSize string + Destination string + Version int +} + +func InsertCartridge(path string) *Cartridge { + file, err := os.Open(path) + if err != nil { + log.Fatal(err) + } + defer file.Close() + cartridge := Cartridge{Filename: file.Name()} + + // Jump to start of header + _, err = file.Seek(0x0100, io.SeekStart) + if err != nil { + log.Fatal(err) + } + + // Read header + var header ROMHeader + err = binary.Read(file, binary.LittleEndian, &header) + if err != nil { + log.Fatal(err) + } + + // Validate the ROM by checking presence of the Nintendo logo + if header.Logo != expectedLogo { + log.Fatal("Invalid ROM file: No valid logo found!") + } + + // Convert some header values + cartridge.Title = string(bytes.Trim(header.Title[:], "\x00")) + cartridge.Mapper = cartridgeTypes[header.CartridgeType] + if header.OldLicenseeCode == 0x33 { + // FIXME(m): Support new licensee codes + cartridge.Licensee = "Indicates that the New licensee code should be used instead." + } else { + cartridge.Licensee = oldLicensees[header.OldLicenseeCode] + } + cartridge.SGBSupport = (header.SGBFlag == 0x03) + cartridge.ROMSize = 32 * (1 << header.ROMSize) + cartridge.RAMSize = ramSizes[header.RAMSize] + switch header.DestinationCode { + case 0x00: + cartridge.Destination = "Japan (and possibly overseas)" + case 0x01: + cartridge.Destination = "Overseas only" + default: + cartridge.Destination = "UNKNOWN" + } + cartridge.Version = int(header.MaskROMVersionNumber) + + // TODO(m): Verify header checksum + + // NOTE(m): Ignoring global checksum which is not used, except by one emulator. + // See https://gbdev.io/pandocs/The_Cartridge_Header.html#014e-014f--global-checksum + + return &cartridge +} diff --git a/gb/console.go b/gb/console.go new file mode 100644 index 0000000..e66ebe6 --- /dev/null +++ b/gb/console.go @@ -0,0 +1,17 @@ +package gb + +type Console struct { + Cartridge *Cartridge +} + +func NewConsole(path string) (*Console, error) { + cartridge := InsertCartridge(path) + + console := Console{cartridge} + + return &console, nil +} + +func (console *Console) Update(dt uint64) { + console.StepSeconds(dt) +} diff --git a/main.go b/main.go index bdea31a..9b83c00 100644 --- a/main.go +++ b/main.go @@ -1,127 +1,16 @@ package main import ( - "fmt" - "log" - "runtime" - - "github.com/veandco/go-sdl2/sdl" - "github.com/veandco/go-sdl2/ttf" + "gb-player/ui" ) -var renderer *sdl.Renderer -var font *ttf.Font - -func init() { - runtime.LockOSThread() -} - func main() { - if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { - log.Fatal(err) - } - defer sdl.Quit() + // FIXME(m): Allow specifying rom file on command line + // if len(os.Args) != 2 { + // log.Fatalln("No rom file specified") + // } + // romPath := os.Args[1] + romPath := "rom.gb" - if err := ttf.Init(); err != nil { - log.Fatal((err)) - } - defer ttf.Quit() - - window, err := sdl.CreateWindow( - "GB Player", - sdl.WINDOWPOS_UNDEFINED, - sdl.WINDOWPOS_UNDEFINED, - 800, 600, - sdl.WINDOW_SHOWN) - if err != nil { - panic(err) - } - defer window.Destroy() - - renderer, err = sdl.CreateRenderer(window, -1, 0) - if err != nil { - panic(err) - } - defer renderer.Destroy() - renderer.RenderSetVSync(true) - - font, err = ttf.OpenFont("SourceCodePro.ttf", 18) - if err != nil { - log.Fatal(err) - } - defer font.Close() - font.SetStyle(ttf.STYLE_BOLD) - - var fps float64 - var frameCount uint32 - var startTicks = sdl.GetTicks64() - var now uint64 - var last uint64 - - cartridge := Insert("rom.gb") - fmt.Println(cartridge) - - running := true - for running { - for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { - switch event.(type) { - case *sdl.QuitEvent: - println("Quit") - running = false - } - } - - last = now - now = sdl.GetPerformanceCounter() - deltaTime := float64((now - last) * 1000.0 / sdl.GetPerformanceFrequency()) - _ = deltaTime - - // Clear screen - renderer.SetDrawColor(0x63, 0x94, 0xED, 0xff) - renderer.Clear() - - // Update state - rect := sdl.Rect{X: 100, Y: 100, W: 200, H: 150} - renderer.SetDrawColor(0, 0, 255, 255) - renderer.FillRect(&rect) - - drawDebugWindow(fps) - - // Present to screen - renderer.Present() - - // Calculate framerate - frameCount++ - currentTicks := sdl.GetTicks64() - elapsed := currentTicks - startTicks - - if elapsed >= 1000 { - fps = float64(frameCount) / (float64(elapsed) / 1000.0) - - // Reset counters - frameCount = 0 - startTicks = currentTicks - } - } -} - -func drawDebugWindow(fps float64) { - var textSurface *sdl.Surface - var textTexture *sdl.Texture - - // FPS - textSurface, err := font.RenderUTF8Blended(fmt.Sprintf("FPS: %.2f", fps), sdl.Color{R: 0, G: 0, B: 0, A: 255}) - if err != nil { - log.Fatal(err) - } - defer textSurface.Free() - - textTexture, err = renderer.CreateTextureFromSurface(textSurface) - if err != nil { - log.Fatal(err) - } - defer textTexture.Destroy() - - textRect := sdl.Rect{X: 0, Y: 0, W: textSurface.W, H: textSurface.H} - renderer.Copy(textTexture, nil, &textRect) + ui.Run(romPath) } diff --git a/ui/controller.go b/ui/controller.go new file mode 100644 index 0000000..5679ce1 --- /dev/null +++ b/ui/controller.go @@ -0,0 +1,86 @@ +package ui + +import ( + "gb-player/gb" + + "github.com/veandco/go-sdl2/sdl" +) + +type Controller struct { + renderer *sdl.Renderer + view *View + timestamp uint64 +} + +func NewController(renderer *sdl.Renderer) *Controller { + controller := Controller{} + controller.renderer = renderer + return &controller +} + +func (c *Controller) Start(path string) { + console := gb.NewConsole(path) + c.view = NewView(c, console) + c.Run() +} + +func (c *Controller) Step() { + timestamp := sdl.GetTicks64() + dt := timestamp - c.timestamp + c.view.Update(timestamp, dt) +} + +func (c *Controller) Run() { + var fps float64 + var frameCount uint32 + var startTicks = sdl.GetTicks64() + var now, last uint64 + + // cartridge := gb.Insert(romPath) + // fmt.Println(cartridge) + + running := true + for running { + for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { + switch event.(type) { + case *sdl.QuitEvent: + println("Quit") + running = false + } + } + + last = now + now = sdl.GetPerformanceCounter() + deltaTime := float64((now - last) * 1000.0 / sdl.GetPerformanceFrequency()) + _ = deltaTime + + // Clear screen + renderer.SetDrawColor(0x63, 0x94, 0xED, 0xff) + renderer.Clear() + + // Update state + c.Step() + rect := sdl.Rect{X: 100, Y: 100, W: 200, H: 150} + renderer.SetDrawColor(0, 0, 255, 255) + renderer.FillRect(&rect) + + drawDebugWindow(fps) + + // Present to screen + renderer.Present() + + // Calculate framerate + frameCount++ + currentTicks := sdl.GetTicks64() + elapsed := currentTicks - startTicks + + if elapsed >= 1000 { + fps = float64(frameCount) / (float64(elapsed) / 1000.0) + + // Reset counters + frameCount = 0 + startTicks = currentTicks + } + } + +} diff --git a/ui/run.go b/ui/run.go new file mode 100644 index 0000000..4b328d7 --- /dev/null +++ b/ui/run.go @@ -0,0 +1,75 @@ +package ui + +import ( + "fmt" + "log" + "runtime" + + "github.com/veandco/go-sdl2/sdl" + "github.com/veandco/go-sdl2/ttf" +) + +var renderer *sdl.Renderer +var font *ttf.Font + +func init() { + runtime.LockOSThread() +} + +func Run(romPath string) { + if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { + log.Fatal(err) + } + defer sdl.Quit() + + if err := ttf.Init(); err != nil { + log.Fatal((err)) + } + defer ttf.Quit() + + window, err := sdl.CreateWindow( + "GB Player", + sdl.WINDOWPOS_UNDEFINED, + sdl.WINDOWPOS_UNDEFINED, + 800, 600, + sdl.WINDOW_SHOWN) + if err != nil { + panic(err) + } + defer window.Destroy() + + renderer, err = sdl.CreateRenderer(window, -1, 0) + if err != nil { + panic(err) + } + defer renderer.Destroy() + renderer.RenderSetVSync(true) + + font, err = ttf.OpenFont("SourceCodePro.ttf", 18) + if err != nil { + log.Fatal(err) + } + defer font.Close() + font.SetStyle(ttf.STYLE_BOLD) + + controller := NewController(renderer) + controller.Start(romPath) +} + +func drawDebugWindow(fps float64) { + // FPS + textSurface, err := font.RenderUTF8Blended(fmt.Sprintf("FPS: %.2f", fps), sdl.Color{R: 0, G: 0, B: 0, A: 255}) + if err != nil { + log.Fatal(err) + } + defer textSurface.Free() + + textTexture, err := renderer.CreateTextureFromSurface(textSurface) + if err != nil { + log.Fatal(err) + } + defer textTexture.Destroy() + + textRect := sdl.Rect{X: 0, Y: 0, W: textSurface.W, H: textSurface.H} + renderer.Copy(textTexture, nil, &textRect) +} diff --git a/ui/view.go b/ui/view.go new file mode 100644 index 0000000..499b478 --- /dev/null +++ b/ui/view.go @@ -0,0 +1,14 @@ +package ui + +import ( + "gb-player/gb" +) + +type View struct { + controller *Controller + console *gb.Console +} + +func NewView(controller *Controller, console *gb.Console) *View { + return &View{controller, console} +}