Compare commits
12 Commits
5387152a21
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b860999dc8 | ||
|
|
7678fda9e7 | ||
|
|
7c494acc7e | ||
|
|
772ff893af | ||
|
|
c18615c629 | ||
|
|
82d3898216 | ||
|
|
1bae615b39 | ||
|
|
6e3149d093 | ||
|
|
b72667947f | ||
|
|
3cb7e3a5c9 | ||
|
|
c3d17459c6 | ||
|
|
ba4e098ba5 |
@@ -1,3 +1,5 @@
|
|||||||
# gb-player
|
# gb-player
|
||||||
|
|
||||||
Fun with Go and GameBoy cartridges
|
Learning the Go Programming Language by making a Game Boy emulator.
|
||||||
|
|
||||||
|
https://hacktheplanet.be/2025/08/29/game-boy-emulator-in-go/
|
||||||
|
|||||||
301
cartridge.go
301
cartridge.go
@@ -1,301 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
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 Insert(filename string) Cartridge {
|
|
||||||
file, err := os.Open(filename)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInsertCartridge(t *testing.T) {
|
|
||||||
cartridge := Insert("rom.gb")
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
assert.Equal(cartridge.Title, "SEIKEN DENSETSU")
|
|
||||||
assert.Equal(cartridge.Mapper, "MBC2+BATTERY")
|
|
||||||
assert.Equal(cartridge.Licensee, "Square")
|
|
||||||
assert.False(cartridge.SGBSupport, "SGB support should be false")
|
|
||||||
assert.Equal(cartridge.ROMSize, 256)
|
|
||||||
assert.Equal(cartridge.RAMSize, "0 - No RAM")
|
|
||||||
assert.Equal(cartridge.Destination, "Overseas only")
|
|
||||||
assert.Equal(cartridge.Version, 0)
|
|
||||||
}
|
|
||||||
21
cpu.go
21
cpu.go
@@ -1,21 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
type Cpu struct {
|
|
||||||
A uint8
|
|
||||||
Flags uint8
|
|
||||||
BC uint16
|
|
||||||
DE uint16
|
|
||||||
HL uint16
|
|
||||||
SP uint16
|
|
||||||
PC uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
func Reset() Cpu {
|
|
||||||
cpu := Cpu{}
|
|
||||||
|
|
||||||
return cpu
|
|
||||||
}
|
|
||||||
|
|
||||||
func Tick(cpu *Cpu) {
|
|
||||||
cpu.PC++
|
|
||||||
}
|
|
||||||
92
gb/bus.go
Normal file
92
gb/bus.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package gb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 0x0000 - 0x3FFF : ROM Bank 0
|
||||||
|
// 0x4000 - 0x7FFF : ROM Bank 1 - Switchable
|
||||||
|
// 0x8000 - 0x97FF : CHR RAM
|
||||||
|
// 0x9800 - 0x9BFF : BG Map 1
|
||||||
|
// 0x9C00 - 0x9FFF : BG Map 2
|
||||||
|
// 0xA000 - 0xBFFF : Cartridge RAM
|
||||||
|
// 0xC000 - 0xCFFF : RAM Bank 0
|
||||||
|
// 0xD000 - 0xDFFF : RAM Bank 1-7 - switchable - Color only
|
||||||
|
// 0xE000 - 0xFDFF : Reserved - Echo RAM
|
||||||
|
// 0xFE00 - 0xFE9F : Object Attribute Memory
|
||||||
|
// 0xFEA0 - 0xFEFF : Reserved - Unusable
|
||||||
|
// 0xFF00 - 0xFF7F : I/O Registers
|
||||||
|
// 0xFF80 - 0xFFFE : Zero Page
|
||||||
|
|
||||||
|
type Bus struct {
|
||||||
|
Cart *Cartridge
|
||||||
|
RAM *RAM
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBus(cart *Cartridge) *Bus {
|
||||||
|
bus := Bus{Cart: cart, RAM: NewRAM()}
|
||||||
|
|
||||||
|
return &bus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bus *Bus) Read(address uint16) byte {
|
||||||
|
if address < 0x8000 {
|
||||||
|
// ROM data
|
||||||
|
value, err := bus.Cart.Read(address)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading from bus address %X: %s\n", address, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
} else if address < 0xE000 {
|
||||||
|
//WRAM (Working RAM)
|
||||||
|
return bus.RAM.WRAMRead(address)
|
||||||
|
} else if address < 0xFF80 {
|
||||||
|
//IO Registers
|
||||||
|
return IORead(address)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Reading from bus address %X not implemented!\n", address)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bus *Bus) Read16(address uint16) uint16 {
|
||||||
|
lo := bus.Read(address)
|
||||||
|
hi := bus.Read(address + 1)
|
||||||
|
|
||||||
|
return uint16(lo) | uint16(hi)<<8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bus *Bus) Write(address uint16, value byte) error {
|
||||||
|
if address < 0x8000 {
|
||||||
|
// ROM data
|
||||||
|
err := bus.Cart.Write(address, value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error writing to bus address %X: %s", address, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else if address < 0xE000 {
|
||||||
|
//WRAM (Working RAM)
|
||||||
|
bus.RAM.WRAMWrite(address, value)
|
||||||
|
return nil
|
||||||
|
} else if address < 0xFF80 {
|
||||||
|
//IO Registers
|
||||||
|
IOWrite(address, value)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Writing to bus address %X not implemented!", address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bus *Bus) Write16(address uint16, value uint16) error {
|
||||||
|
err := bus.Write(address+1, byte((value>>8)&0xFF))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = bus.Write(address, byte(value&0xFF))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,22 +3,11 @@ package gb
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0104-0133--nintendo-logo
|
// Cartridge types aka Mappers
|
||||||
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
|
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0147--cartridge-type
|
||||||
var cartridgeTypes = map[byte]string{
|
var cartridgeTypes = map[byte]string{
|
||||||
0x00: "ROM ONLY",
|
0x00: "ROM ONLY",
|
||||||
@@ -215,6 +204,7 @@ var oldLicensees = map[byte]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ROMHeader struct {
|
type ROMHeader struct {
|
||||||
|
_ [256]byte
|
||||||
EntryPoint [4]byte
|
EntryPoint [4]byte
|
||||||
Logo [48]byte
|
Logo [48]byte
|
||||||
// NOTE(m): Assuming "old" cartridges here. This may cause problems with newer cartridges.
|
// NOTE(m): Assuming "old" cartridges here. This may cause problems with newer cartridges.
|
||||||
@@ -228,11 +218,13 @@ type ROMHeader struct {
|
|||||||
DestinationCode byte
|
DestinationCode byte
|
||||||
OldLicenseeCode byte
|
OldLicenseeCode byte
|
||||||
MaskROMVersionNumber byte
|
MaskROMVersionNumber byte
|
||||||
HeaderChecksum byte
|
Checksum byte
|
||||||
GlobalChecksum [2]byte
|
GlobalChecksum [2]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cartridge struct {
|
type Cartridge struct {
|
||||||
|
Header *ROMHeader
|
||||||
|
Data []byte
|
||||||
Filename string
|
Filename string
|
||||||
Mapper string
|
Mapper string
|
||||||
Title string
|
Title string
|
||||||
@@ -242,60 +234,67 @@ type Cartridge struct {
|
|||||||
RAMSize string
|
RAMSize string
|
||||||
Destination string
|
Destination string
|
||||||
Version int
|
Version int
|
||||||
|
Checksum byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func InsertCartridge(path string) *Cartridge {
|
func InsertCartridge(filename string) (*Cartridge, error) {
|
||||||
file, err := os.Open(path)
|
cart := Cartridge{Filename: filename}
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
cartridge := Cartridge{Filename: file.Name()}
|
|
||||||
|
|
||||||
// Jump to start of header
|
data, err := os.ReadFile(filename)
|
||||||
_, err = file.Seek(0x0100, io.SeekStart)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return &cart, err
|
||||||
}
|
}
|
||||||
|
cart.Data = data
|
||||||
|
|
||||||
// Read header
|
// Read header
|
||||||
var header ROMHeader
|
var header ROMHeader
|
||||||
err = binary.Read(file, binary.LittleEndian, &header)
|
buffer := bytes.NewReader(cart.Data)
|
||||||
|
err = binary.Read(buffer, binary.LittleEndian, &header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return &cart, nil
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the ROM by checking presence of the Nintendo logo
|
|
||||||
if header.Logo != expectedLogo {
|
|
||||||
log.Fatal("Invalid ROM file: No valid logo found!")
|
|
||||||
}
|
}
|
||||||
|
cart.Header = &header
|
||||||
|
|
||||||
// Convert some header values
|
// Convert some header values
|
||||||
cartridge.Title = string(bytes.Trim(header.Title[:], "\x00"))
|
cart.Title = string(bytes.Trim(header.Title[:], "\x00"))
|
||||||
cartridge.Mapper = cartridgeTypes[header.CartridgeType]
|
cart.Mapper = cartridgeTypes[header.CartridgeType]
|
||||||
if header.OldLicenseeCode == 0x33 {
|
if header.OldLicenseeCode == 0x33 {
|
||||||
// FIXME(m): Support new licensee codes
|
// FIXME(m): Support new licensee codes
|
||||||
cartridge.Licensee = "Indicates that the New licensee code should be used instead."
|
cart.Licensee = "Indicates that the New licensee code should be used instead."
|
||||||
} else {
|
} else {
|
||||||
cartridge.Licensee = oldLicensees[header.OldLicenseeCode]
|
cart.Licensee = oldLicensees[header.OldLicenseeCode]
|
||||||
}
|
}
|
||||||
cartridge.SGBSupport = (header.SGBFlag == 0x03)
|
cart.SGBSupport = (header.SGBFlag == 0x03)
|
||||||
cartridge.ROMSize = 32 * (1 << header.ROMSize)
|
cart.ROMSize = 32 * (1 << header.ROMSize)
|
||||||
cartridge.RAMSize = ramSizes[header.RAMSize]
|
cart.RAMSize = ramSizes[header.RAMSize]
|
||||||
switch header.DestinationCode {
|
switch header.DestinationCode {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
cartridge.Destination = "Japan (and possibly overseas)"
|
cart.Destination = "Japan (and possibly overseas)"
|
||||||
case 0x01:
|
case 0x01:
|
||||||
cartridge.Destination = "Overseas only"
|
cart.Destination = "Overseas only"
|
||||||
default:
|
default:
|
||||||
cartridge.Destination = "UNKNOWN"
|
cart.Destination = "UNKNOWN"
|
||||||
}
|
}
|
||||||
cartridge.Version = int(header.MaskROMVersionNumber)
|
cart.Version = int(header.MaskROMVersionNumber)
|
||||||
|
|
||||||
// TODO(m): Verify header checksum
|
// Calculate and verify checksum
|
||||||
|
for address := uint16(0x0134); address <= uint16(0x014C); address++ {
|
||||||
|
cart.Checksum = cart.Checksum - cart.Data[address] - 1
|
||||||
|
}
|
||||||
|
if cart.Checksum != header.Checksum {
|
||||||
|
return &cart, fmt.Errorf("ROM checksum failed: %X does not equal %X", cart.Checksum, header.Checksum)
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE(m): Ignoring global checksum which is not used, except by one emulator.
|
// 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
|
// See https://gbdev.io/pandocs/The_Cartridge_Header.html#014e-014f--global-checksum
|
||||||
|
|
||||||
return &cartridge
|
return &cart, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cart *Cartridge) Read(address uint16) (byte, error) {
|
||||||
|
return cart.Data[address], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cart *Cartridge) Write(address uint16, value byte) error {
|
||||||
|
return fmt.Errorf("Writing to cartridge address %X not implemented!", address)
|
||||||
}
|
}
|
||||||
|
|||||||
31
gb/cartridge_test.go
Normal file
31
gb/cartridge_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package gb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInsert(t *testing.T) {
|
||||||
|
cartridge, err := InsertCartridge("../roms/dmg-acid2.gb")
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(cartridge.Filename, "../roms/dmg-acid2.gb")
|
||||||
|
assert.Equal(cartridge.Title, "DMG-ACID2")
|
||||||
|
assert.Equal(cartridge.Mapper, "ROM ONLY")
|
||||||
|
assert.Equal(cartridge.Licensee, "None")
|
||||||
|
assert.False(cartridge.SGBSupport, "SGB support should be false")
|
||||||
|
assert.Equal(cartridge.ROMSize, 32)
|
||||||
|
assert.Equal(cartridge.RAMSize, "0 - No RAM")
|
||||||
|
assert.Equal(cartridge.Destination, "Japan (and possibly overseas)")
|
||||||
|
assert.Equal(cartridge.Version, 0)
|
||||||
|
assert.Equal(cartridge.Checksum, byte(0x9F))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailedChecksum(t *testing.T) {
|
||||||
|
_, err := InsertCartridge("../roms/failed-checksum.gb")
|
||||||
|
|
||||||
|
assert.EqualError(t, err, "ROM checksum failed: 9F does not equal 41")
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package gb
|
package gb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
)
|
)
|
||||||
@@ -11,30 +12,61 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Console struct {
|
type Console struct {
|
||||||
Cartridge *Cartridge
|
Bus *Bus
|
||||||
front *image.RGBA
|
CPU *CPU
|
||||||
|
front *image.RGBA
|
||||||
|
Ticks uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsole(path string) (*Console, error) {
|
func NewConsole(path string) (*Console, error) {
|
||||||
cartridge := InsertCartridge(path)
|
cart, err := InsertCartridge(path)
|
||||||
|
if err != nil {
|
||||||
|
return &Console{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Cartridge loaded:")
|
||||||
|
fmt.Printf("\t Title : %s\n", cart.Title)
|
||||||
|
fmt.Printf("\t Type : %02X (%s)\n", cart.Header.CartridgeType, cart.Mapper)
|
||||||
|
fmt.Printf("\t ROM Size : %d KB\n", cart.ROMSize)
|
||||||
|
fmt.Printf("\t RAM Size : %02X (%s)\n", cart.Header.RAMSize, cart.RAMSize)
|
||||||
|
fmt.Printf("\t LIC Code : %02X (%s)\n", cart.Header.OldLicenseeCode, cart.Licensee)
|
||||||
|
fmt.Printf("\t ROM Vers : %02X\n", cart.Header.MaskROMVersionNumber)
|
||||||
|
fmt.Printf("\t Checksum : %02X ", cart.Checksum)
|
||||||
|
if cart.Checksum == cart.Header.Checksum {
|
||||||
|
fmt.Printf("(PASSED)\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("(FAILED)\n")
|
||||||
|
}
|
||||||
|
|
||||||
buffer := image.NewRGBA(image.Rect(0, 0, ConsoleWidth, ConsoleHeight))
|
buffer := image.NewRGBA(image.Rect(0, 0, ConsoleWidth, ConsoleHeight))
|
||||||
|
|
||||||
console := Console{Cartridge: cartridge, front: buffer}
|
bus := NewBus(cart)
|
||||||
|
console := Console{Bus: bus, CPU: NewCPU(bus), front: buffer}
|
||||||
|
|
||||||
return &console, nil
|
return &console, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (console *Console) StepMilliSeconds(ms uint64) {
|
func (console *Console) StepMilliSeconds(ms uint64) {
|
||||||
speed := int(ms / 3)
|
speed := int(ms / 3)
|
||||||
for y := 0; y < ConsoleHeight; y++ {
|
for y := range ConsoleHeight {
|
||||||
for x := 0; x < ConsoleWidth; x++ {
|
for x := range ConsoleWidth {
|
||||||
console.front.Set(x, y, color.RGBA{0, uint8(y + speed), uint8(x + speed), 255})
|
console.front.Set(x, y, color.RGBA{0, uint8(y + speed), uint8(x + speed), 255})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (console *Console) Buffer() *image.RGBA {
|
func (console *Console) Buffer() *image.RGBA {
|
||||||
|
|
||||||
return console.front
|
return console.front
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (console *Console) Cycle(cycles int) {
|
||||||
|
for range cycles {
|
||||||
|
// Cycles are given in M-cycles (machine cycles) instead of T-states (system clock ticks)
|
||||||
|
// One machine cycle takes 4 system clock ticks to complete
|
||||||
|
for range 4 {
|
||||||
|
console.Ticks++
|
||||||
|
timer.Tick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
382
gb/cpu.go
Normal file
382
gb/cpu.go
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
package gb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CPUFrequency = 4194304
|
||||||
|
|
||||||
|
type CPUFlags byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
C CPUFlags = 1 << 4 // Carry Flag
|
||||||
|
H CPUFlags = 1 << 5 // Half Carry Flag
|
||||||
|
N CPUFlags = 1 << 6 // Subtract Flag
|
||||||
|
Z CPUFlags = 1 << 7 // Zero Flag
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registers struct {
|
||||||
|
A byte
|
||||||
|
F CPUFlags
|
||||||
|
B byte
|
||||||
|
C byte
|
||||||
|
D byte
|
||||||
|
E byte
|
||||||
|
H byte
|
||||||
|
L byte
|
||||||
|
PC uint16
|
||||||
|
SP uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type CPU struct {
|
||||||
|
Bus *Bus
|
||||||
|
Regs Registers
|
||||||
|
Halted bool
|
||||||
|
Stepping bool
|
||||||
|
InterruptMasterEnable bool
|
||||||
|
InterruptFlags byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCPU(bus *Bus) *CPU {
|
||||||
|
cpu := CPU{}
|
||||||
|
cpu.Bus = bus
|
||||||
|
// NOTE(m): PC is usually set to 0x100 by the boot rom
|
||||||
|
// TODO(m): SP is usually set programmatically by the cartridge code.
|
||||||
|
// Remove this hardcoded value later!
|
||||||
|
cpu.Regs = Registers{SP: 0xDFFF}
|
||||||
|
cpu.Stepping = true
|
||||||
|
|
||||||
|
return &cpu
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) Step() {
|
||||||
|
if !cpu.Halted {
|
||||||
|
opcode := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
|
||||||
|
fmt.Printf("%04X: (%02X %02X %02X) A: %02X B: %02X C: %02X D: %02X E: %02X H: %02X L: %02X\n", cpu.Regs.PC,
|
||||||
|
opcode, cpu.Bus.Read(cpu.Regs.PC+1), cpu.Bus.Read(cpu.Regs.PC+2), cpu.Regs.A, cpu.Regs.B, cpu.Regs.C,
|
||||||
|
cpu.Regs.D, cpu.Regs.E, cpu.Regs.H, cpu.Regs.L)
|
||||||
|
|
||||||
|
cpu.Regs.PC++
|
||||||
|
|
||||||
|
switch opcode {
|
||||||
|
|
||||||
|
case 0x00:
|
||||||
|
// NOP
|
||||||
|
|
||||||
|
case 0x0D:
|
||||||
|
// DEC C
|
||||||
|
cpu.Regs.C--
|
||||||
|
|
||||||
|
// Set appropriate flags
|
||||||
|
if cpu.Regs.C == 0 {
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.SetFlag(N)
|
||||||
|
|
||||||
|
if (cpu.Regs.C & 0x0F) == 0x0F {
|
||||||
|
cpu.SetFlag(H)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(H)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x0E:
|
||||||
|
// LD C, n8
|
||||||
|
val := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.C = val
|
||||||
|
cpu.Regs.PC++
|
||||||
|
|
||||||
|
case 0x10:
|
||||||
|
// STOP n8
|
||||||
|
cpu.Halted = true
|
||||||
|
|
||||||
|
case 0x11:
|
||||||
|
// LD DE, n16
|
||||||
|
cpu.Regs.E = cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.D = cpu.Bus.Read(cpu.Regs.PC + 1)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.PC += 2
|
||||||
|
|
||||||
|
case 0x12:
|
||||||
|
// LD [DE], A
|
||||||
|
// Get 16-bit address from DE
|
||||||
|
address := uint16(cpu.Regs.D)<<8 | uint16(cpu.Regs.E)
|
||||||
|
cpu.Bus.Write(address, cpu.Regs.A)
|
||||||
|
|
||||||
|
case 0x14:
|
||||||
|
// INC D
|
||||||
|
cpu.Regs.D++
|
||||||
|
|
||||||
|
// Set appropriate flags
|
||||||
|
if cpu.Regs.D == 0 {
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.ClearFlag(N)
|
||||||
|
|
||||||
|
if (cpu.Regs.D & 0x0F) == 0 {
|
||||||
|
cpu.SetFlag(H)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(H)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x18:
|
||||||
|
// JR e8
|
||||||
|
// Jump relative to 8-bit signed offset
|
||||||
|
// emu_cycles(3);
|
||||||
|
offset := int8(cpu.Bus.Read(cpu.Regs.PC))
|
||||||
|
cpu.Regs.PC++
|
||||||
|
cpu.Regs.PC = uint16(int(cpu.Regs.PC) + int(offset))
|
||||||
|
|
||||||
|
case 0x1C:
|
||||||
|
// INC E
|
||||||
|
cpu.Regs.E++
|
||||||
|
|
||||||
|
// Set appropriate flags
|
||||||
|
if cpu.Regs.E == 0 {
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.ClearFlag(N)
|
||||||
|
|
||||||
|
if (cpu.Regs.E & 0x0F) == 0 {
|
||||||
|
cpu.SetFlag(H)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(H)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x20:
|
||||||
|
// JR NZ, e8
|
||||||
|
if cpu.IsFlagSet(Z) {
|
||||||
|
// Z is set, don't execute
|
||||||
|
// emu_cycles(2);
|
||||||
|
cpu.Regs.PC++
|
||||||
|
} else {
|
||||||
|
// Z is not set, execute
|
||||||
|
// Jump relative to 8-bit signed offset
|
||||||
|
// emu_cycles(3);
|
||||||
|
offset := int8(cpu.Bus.Read(cpu.Regs.PC))
|
||||||
|
cpu.Regs.PC++
|
||||||
|
cpu.Regs.PC = uint16(int(cpu.Regs.PC) + int(offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x21:
|
||||||
|
// LD HL, n16
|
||||||
|
cpu.Regs.L = cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.H = cpu.Bus.Read(cpu.Regs.PC + 1)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.PC += 2
|
||||||
|
|
||||||
|
case 0x2A:
|
||||||
|
// LD A, [HL+] or LD A, [HLI]
|
||||||
|
// Get 16-bit address from HL
|
||||||
|
address := uint16(cpu.Regs.H)<<8 | uint16(cpu.Regs.L)
|
||||||
|
// Read byte at address and assign value to A register
|
||||||
|
cpu.Regs.A = cpu.Bus.Read(address)
|
||||||
|
// emu_cycles(1);
|
||||||
|
// Increment HL
|
||||||
|
address++
|
||||||
|
cpu.Regs.H = byte(address >> 8)
|
||||||
|
cpu.Regs.L = byte(address)
|
||||||
|
// emu_cycles(1);
|
||||||
|
|
||||||
|
case 0x31:
|
||||||
|
// LD SP, n16
|
||||||
|
lo := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
// emu_cycles(1);
|
||||||
|
hi := cpu.Bus.Read(cpu.Regs.PC + 1)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.SP = uint16(lo) | uint16(hi)<<8
|
||||||
|
cpu.Regs.PC += 2
|
||||||
|
|
||||||
|
case 0x3C:
|
||||||
|
// INC A
|
||||||
|
cpu.Regs.A++
|
||||||
|
|
||||||
|
// Set appropriate flags
|
||||||
|
if cpu.Regs.A == 0 {
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.ClearFlag(N)
|
||||||
|
|
||||||
|
if (cpu.Regs.A & 0x0F) == 0 {
|
||||||
|
cpu.SetFlag(H)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(H)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x3E:
|
||||||
|
// LD A, n8
|
||||||
|
val := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.A = val
|
||||||
|
cpu.Regs.PC++
|
||||||
|
|
||||||
|
case 0x47:
|
||||||
|
// LD B, A
|
||||||
|
cpu.Regs.B = cpu.Regs.A
|
||||||
|
|
||||||
|
case 0x78:
|
||||||
|
// LD A, B
|
||||||
|
cpu.Regs.A = cpu.Regs.B
|
||||||
|
|
||||||
|
case 0x7C:
|
||||||
|
// LD A, H
|
||||||
|
cpu.Regs.A = cpu.Regs.H
|
||||||
|
|
||||||
|
case 0x7D:
|
||||||
|
// LD A, L
|
||||||
|
cpu.Regs.A = cpu.Regs.L
|
||||||
|
|
||||||
|
case 0xCB:
|
||||||
|
// Prefix byte instructions
|
||||||
|
cbOpcode := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
|
||||||
|
fmt.Printf("%04X: (%02X %02X %02X) A: %02X B: %02X C: %02X\n", cpu.Regs.PC,
|
||||||
|
cbOpcode, cpu.Bus.Read(cpu.Regs.PC+1), cpu.Bus.Read(cpu.Regs.PC+2), cpu.Regs.A, cpu.Regs.B, cpu.Regs.C)
|
||||||
|
|
||||||
|
cpu.Regs.PC++
|
||||||
|
|
||||||
|
switch cbOpcode {
|
||||||
|
// case 0x7E:
|
||||||
|
// // BIT 7, [HL]
|
||||||
|
// // Read byte pointed to by address HL
|
||||||
|
// address := uint16(cpu.Regs.H)<<8 | uint16(cpu.Regs.L)
|
||||||
|
// val := cpu.Bus.Read(address)
|
||||||
|
|
||||||
|
// // Check if bit 7 is set
|
||||||
|
// if (val & 0x80) == 0 {
|
||||||
|
// // Set zero flag if bit is not set
|
||||||
|
// cpu.SetFlag(Z)
|
||||||
|
// }
|
||||||
|
// cpu.ClearFlag(N)
|
||||||
|
// cpu.SetFlag(H)
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("\nINVALID INSTRUCTION! Unknown CB opcode: %02X\n", cbOpcode)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0xC3:
|
||||||
|
// JP a16
|
||||||
|
lo := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
// emu_cycles(1);
|
||||||
|
hi := cpu.Bus.Read(cpu.Regs.PC + 1)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.PC = uint16(lo) | uint16(hi)<<8
|
||||||
|
|
||||||
|
case 0xC9:
|
||||||
|
// RET
|
||||||
|
// emu_cycles(4);
|
||||||
|
cpu.Regs.PC = cpu.StackPop16()
|
||||||
|
|
||||||
|
case 0xCD:
|
||||||
|
// CALL a16
|
||||||
|
cpu.StackPush16(cpu.Regs.PC + 2)
|
||||||
|
|
||||||
|
lo := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
// emu_cycles(1);
|
||||||
|
hi := cpu.Bus.Read(cpu.Regs.PC + 1)
|
||||||
|
// emu_cycles(1);
|
||||||
|
cpu.Regs.PC = uint16(lo) | uint16(hi)<<8
|
||||||
|
|
||||||
|
case 0xE0:
|
||||||
|
// LDH [a8], A
|
||||||
|
offset := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
address := 0xFF00 | uint16(offset)
|
||||||
|
cpu.Bus.Write(address, cpu.Regs.A)
|
||||||
|
cpu.Regs.PC++
|
||||||
|
|
||||||
|
case 0xE5:
|
||||||
|
// PUSH HL
|
||||||
|
// emu_cycles(4);
|
||||||
|
cpu.StackPush(cpu.Regs.H)
|
||||||
|
cpu.StackPush(cpu.Regs.L)
|
||||||
|
|
||||||
|
case 0xE9:
|
||||||
|
// JP HL
|
||||||
|
val := uint16(cpu.Regs.H)<<8 | uint16(cpu.Regs.L)
|
||||||
|
cpu.Regs.PC = val
|
||||||
|
|
||||||
|
case 0xEA:
|
||||||
|
// LD [a16], A
|
||||||
|
lo := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
cpu.Regs.PC++
|
||||||
|
hi := cpu.Bus.Read(cpu.Regs.PC)
|
||||||
|
cpu.Regs.PC++
|
||||||
|
|
||||||
|
address := uint16(lo) | uint16(hi)<<8
|
||||||
|
cpu.Bus.Write(address, cpu.Regs.A)
|
||||||
|
|
||||||
|
case 0xF3:
|
||||||
|
// DI
|
||||||
|
cpu.InterruptMasterEnable = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("\nINVALID INSTRUCTION! Unknown opcode: %02X\n", opcode)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) SetFlag(flag CPUFlags) {
|
||||||
|
cpu.Regs.F |= flag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) ClearFlag(flag CPUFlags) {
|
||||||
|
cpu.Regs.F &^= flag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) ToggleFlag(flag CPUFlags) {
|
||||||
|
cpu.Regs.F ^= flag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) IsFlagSet(flag CPUFlags) bool {
|
||||||
|
return cpu.Regs.F&flag != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) StackPush(data byte) {
|
||||||
|
cpu.Regs.SP--
|
||||||
|
cpu.Bus.Write(cpu.Regs.SP, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) StackPush16(data uint16) {
|
||||||
|
cpu.StackPush(byte((data >> 8) & 0xFF))
|
||||||
|
cpu.StackPush(byte(data & 0xFF))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) StackPop() byte {
|
||||||
|
val := cpu.Bus.Read(cpu.Regs.SP)
|
||||||
|
cpu.Regs.SP++
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) StackPop16() uint16 {
|
||||||
|
lo := cpu.StackPop()
|
||||||
|
hi := cpu.StackPop()
|
||||||
|
|
||||||
|
return uint16(hi)<<8 | uint16(lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) GetInterruptFlags() byte {
|
||||||
|
return cpu.InterruptFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) SetInterruptFlags(value byte) {
|
||||||
|
cpu.InterruptFlags = value
|
||||||
|
}
|
||||||
520
gb/cpu_test.go
Normal file
520
gb/cpu_test.go
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
package gb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createCPU(data []byte) *CPU {
|
||||||
|
cart := Cartridge{Filename: "test"}
|
||||||
|
cart.Data = data
|
||||||
|
cpu := NewCPU(NewBus(&cart))
|
||||||
|
cpu.Regs.PC = 0
|
||||||
|
|
||||||
|
return cpu
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetFlag(t *testing.T) {
|
||||||
|
cartridge, _ := InsertCartridge("../roms/dmg-acid2.gb")
|
||||||
|
bus := NewBus(cartridge)
|
||||||
|
cpu := NewCPU(bus)
|
||||||
|
|
||||||
|
cpu.SetFlag(C)
|
||||||
|
|
||||||
|
assert.True(t, cpu.IsFlagSet(C))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClearFlag(t *testing.T) {
|
||||||
|
cartridge, _ := InsertCartridge("../roms/dmg-acid2.gb")
|
||||||
|
bus := NewBus(cartridge)
|
||||||
|
cpu := NewCPU(bus)
|
||||||
|
|
||||||
|
cpu.SetFlag(C)
|
||||||
|
assert.True(t, cpu.IsFlagSet(C))
|
||||||
|
|
||||||
|
cpu.ClearFlag(C)
|
||||||
|
assert.False(t, cpu.IsFlagSet(C))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToggleFlag(t *testing.T) {
|
||||||
|
cartridge, _ := InsertCartridge("../roms/dmg-acid2.gb")
|
||||||
|
bus := NewBus(cartridge)
|
||||||
|
cpu := NewCPU(bus)
|
||||||
|
|
||||||
|
cpu.ToggleFlag(C)
|
||||||
|
assert.True(t, cpu.IsFlagSet(C))
|
||||||
|
|
||||||
|
cpu.ToggleFlag(C)
|
||||||
|
assert.False(t, cpu.IsFlagSet(C))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction00(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x00, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(0x01), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction3C(t *testing.T) {
|
||||||
|
// Should increment A register
|
||||||
|
cpu := createCPU([]byte{0x3C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.Equal(t, byte(0x01), cpu.Regs.A)
|
||||||
|
|
||||||
|
// Should clear Zero Flag
|
||||||
|
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(Z))
|
||||||
|
|
||||||
|
// Should set Zero Flag
|
||||||
|
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
|
||||||
|
cpu.Regs.A = 0xFF
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(Z))
|
||||||
|
|
||||||
|
// Should clear Subtract Flag
|
||||||
|
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(N)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(N))
|
||||||
|
|
||||||
|
// Should set Half Carry Flag if we overflow from bit 3
|
||||||
|
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
|
||||||
|
cpu.Regs.A = 0x0F
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(H))
|
||||||
|
|
||||||
|
// Should clear Half Carry Flag if we don't overflow from bit 3
|
||||||
|
cpu = createCPU([]byte{0x3C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(H)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(H))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstructionC3(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0xC3, 0x50, 0x01})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(0x150), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstructionE9(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0xE9, 0x00, 0x00})
|
||||||
|
cpu.Regs.H = 0x12
|
||||||
|
cpu.Regs.L = 0x34
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(0x1234), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstructionF3(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0xF3, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.InterruptMasterEnable = true
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.False(t, cpu.InterruptMasterEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction31(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x31, 0xFF, 0xCF})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load SP with n16 value
|
||||||
|
assert.Equal(t, uint16(0xCFFF), cpu.Regs.SP)
|
||||||
|
|
||||||
|
// Should step over the 16 bit value onto the next instruction, i.e. increase the program counter with 3.
|
||||||
|
assert.Equal(t, uint16(0x0003), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstructionCD(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0xCD, 0x4C, 0x48, 0x41})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should push the address of the next instruction onto the stack
|
||||||
|
// In this case 0x41 which is at address 0x03 (i.e. the 4th instruction in the row above creating the CPU instance)
|
||||||
|
assert.Equal(t, uint16(0x03), cpu.Bus.Read16(cpu.Regs.SP))
|
||||||
|
|
||||||
|
// Should jump to n16 value
|
||||||
|
assert.Equal(t, uint16(0x484C), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction21(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x21, 0x34, 0x12})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load the 16-bit immediate value into the combined HL register
|
||||||
|
assert.Equal(t, byte(0x12), cpu.Regs.H)
|
||||||
|
assert.Equal(t, byte(0x34), cpu.Regs.L)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x3), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func TestInstructionCB7E(t *testing.T) {
|
||||||
|
// cpu := createCPU([]byte{0xCB, 0x7E, 0x00, 0x00})
|
||||||
|
|
||||||
|
// cpu.Regs.H = 0xFF
|
||||||
|
// cpu.Regs.L = 0x40
|
||||||
|
// err := cpu.Bus.Write(0xFF40, 0xFF)
|
||||||
|
// assert.Equal(t, err, nil)
|
||||||
|
|
||||||
|
// // FIXME(m): This needs bus access to IO, in particular the LCD
|
||||||
|
// // at 0xFF40.
|
||||||
|
|
||||||
|
// // assert.Equal(t, 0xFF, cpu.Bus.Read(0xFF40))
|
||||||
|
|
||||||
|
// // cpu.Step()
|
||||||
|
|
||||||
|
// // // Should set the zero flag
|
||||||
|
// // assert.True(t, cpu.IsFlagSet(Z))
|
||||||
|
// }
|
||||||
|
|
||||||
|
func TestInstruction47(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x47, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.Regs.A = 0xDE
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load value in register A into register B
|
||||||
|
assert.Equal(t, byte(0xDE), cpu.Regs.B)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction11(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x11, 0x34, 0x12})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load the 16-bit immediate value into the combined DE register
|
||||||
|
assert.Equal(t, byte(0x12), cpu.Regs.D)
|
||||||
|
assert.Equal(t, byte(0x34), cpu.Regs.E)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x3), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction0E(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x0E, 0xDE, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load the 8-bit immediate value into register C
|
||||||
|
assert.Equal(t, byte(0xDE), cpu.Regs.C)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x2), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction2A(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x2A, 0x00, 0x00})
|
||||||
|
cpu.Regs.H = 0xD1
|
||||||
|
cpu.Regs.L = 0x23
|
||||||
|
cpu.Bus.Write(0xD123, 0xDE)
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load the byte pointed to by HL into register A
|
||||||
|
assert.Equal(t, byte(0xDE), cpu.Regs.A)
|
||||||
|
|
||||||
|
// Should increment HL
|
||||||
|
hl := uint16(cpu.Regs.H)<<8 | uint16(cpu.Regs.L)
|
||||||
|
assert.Equal(t, uint16(0xD124), hl)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction12(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x12, 0x00, 0x00})
|
||||||
|
cpu.Regs.D = 0xD1
|
||||||
|
cpu.Regs.E = 0x23
|
||||||
|
cpu.Regs.A = 0xDE
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should copy the value in register A into the byte pointed to by DE
|
||||||
|
address := uint16(cpu.Regs.D)<<8 | uint16(cpu.Regs.E)
|
||||||
|
assert.Equal(t, byte(0xDE), cpu.Bus.Read(address))
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction1C(t *testing.T) {
|
||||||
|
// Should increment E register
|
||||||
|
cpu := createCPU([]byte{0x1C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.Equal(t, byte(0x01), cpu.Regs.E)
|
||||||
|
|
||||||
|
// Should clear Zero Flag
|
||||||
|
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(Z))
|
||||||
|
|
||||||
|
// Should set Zero Flag
|
||||||
|
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
|
||||||
|
cpu.Regs.E = 0xFF
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(Z))
|
||||||
|
|
||||||
|
// Should clear Subtract Flag
|
||||||
|
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(N)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(N))
|
||||||
|
|
||||||
|
// Should set Half Carry Flag if we overflow from bit 3
|
||||||
|
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
|
||||||
|
cpu.Regs.E = 0x0F
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(H))
|
||||||
|
|
||||||
|
// Should clear Half Carry Flag if we don't overflow from bit 3
|
||||||
|
cpu = createCPU([]byte{0x1C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(H)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(H))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction20(t *testing.T) {
|
||||||
|
// Should not jump if Z is set
|
||||||
|
cpu := createCPU([]byte{0x20, 0xFB, 0x00})
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(0x02), cpu.Regs.PC)
|
||||||
|
|
||||||
|
// Should jump to positive offset if Z is not set
|
||||||
|
cpu = createCPU([]byte{0x20, 0x0A, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(0x0C), cpu.Regs.PC)
|
||||||
|
|
||||||
|
// Should jump to negative offset if Z is not set
|
||||||
|
cpu = createCPU([]byte{0x20, 0xFB, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(0xFFFD), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction10(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x10, 0x2A, 0x12})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.True(t, cpu.Halted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction14(t *testing.T) {
|
||||||
|
// Should increment D register
|
||||||
|
cpu := createCPU([]byte{0x14, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.Equal(t, byte(0x01), cpu.Regs.D)
|
||||||
|
|
||||||
|
// Should clear Zero Flag
|
||||||
|
cpu = createCPU([]byte{0x14, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(Z))
|
||||||
|
|
||||||
|
// Should set Zero Flag
|
||||||
|
cpu = createCPU([]byte{0x14, 0x00, 0x00})
|
||||||
|
cpu.Regs.D = 0xFF
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(Z))
|
||||||
|
|
||||||
|
// Should clear Subtract Flag
|
||||||
|
cpu = createCPU([]byte{0x14, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(N)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(N))
|
||||||
|
|
||||||
|
// Should set Half Carry Flag if we overflow from bit 3
|
||||||
|
cpu = createCPU([]byte{0x14, 0x00, 0x00})
|
||||||
|
cpu.Regs.D = 0x0F
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(H))
|
||||||
|
|
||||||
|
// Should clear Half Carry Flag if we don't overflow from bit 3
|
||||||
|
cpu = createCPU([]byte{0x14, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(H)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(H))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction0D(t *testing.T) {
|
||||||
|
// Should decrement C register
|
||||||
|
cpu := createCPU([]byte{0x0D, 0x00, 0x00})
|
||||||
|
cpu.Regs.C = 0x01
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.Equal(t, byte(0x00), cpu.Regs.C)
|
||||||
|
|
||||||
|
// Should clear Zero Flag
|
||||||
|
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(Z)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(Z))
|
||||||
|
|
||||||
|
// Should set Zero Flag
|
||||||
|
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
|
||||||
|
cpu.Regs.C = 0x01
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(Z))
|
||||||
|
|
||||||
|
// Should set Subtract Flag
|
||||||
|
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.SetFlag(N)
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(N))
|
||||||
|
|
||||||
|
// Should set Half Carry Flag if we overflow from bit 3
|
||||||
|
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
|
||||||
|
cpu.Regs.C = 0x10
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
assert.True(t, cpu.IsFlagSet(H))
|
||||||
|
|
||||||
|
// Should clear Half Carry Flag if we don't overflow from bit 3
|
||||||
|
cpu = createCPU([]byte{0x0D, 0x00, 0x00})
|
||||||
|
cpu.Regs.C = 0x01
|
||||||
|
|
||||||
|
cpu.SetFlag(H)
|
||||||
|
cpu.Step()
|
||||||
|
assert.False(t, cpu.IsFlagSet(H))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction78(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x78, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.Regs.B = 0xDE
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load value in register B into register A
|
||||||
|
assert.Equal(t, byte(0xDE), cpu.Regs.A)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstructionEA(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0xEA, 0x23, 0xD1})
|
||||||
|
cpu.Regs.A = 0x42
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load byte in register A into the memory location pointed to by the 16-bit immediate value
|
||||||
|
val := cpu.Bus.Read(0xD123)
|
||||||
|
assert.Equal(t, byte(0x42), val)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x3), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction3E(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x3E, 0xDE, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load the 8-bit immediate value into register A
|
||||||
|
assert.Equal(t, byte(0xDE), cpu.Regs.A)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x2), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstructionE0(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0xE0, 0x07, 0x00})
|
||||||
|
cpu.Regs.A = 0x42
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should copy the value in register A into the byte at address 0xFF00 + 0x07
|
||||||
|
val := cpu.Bus.Read(0xFF07)
|
||||||
|
assert.Equal(t, byte(0x42), val)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x2), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction7D(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x7D, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.Regs.L = 0xDE
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load into register A the value in register L
|
||||||
|
assert.Equal(t, byte(0xDE), cpu.Regs.A)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction7C(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x7C, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.Regs.H = 0xDE
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
// Should load into register A the value in register H
|
||||||
|
assert.Equal(t, byte(0xDE), cpu.Regs.A)
|
||||||
|
|
||||||
|
// Should increase the stack pointer
|
||||||
|
assert.Equal(t, uint16(0x1), cpu.Regs.PC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstruction18(t *testing.T) {
|
||||||
|
// Should jump to positive offset
|
||||||
|
cpu := createCPU([]byte{0x18, 0x0A, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(0x0C), cpu.Regs.PC)
|
||||||
|
|
||||||
|
// Should jump to negative offset
|
||||||
|
cpu = createCPU([]byte{0x18, 0xFB, 0x00})
|
||||||
|
|
||||||
|
cpu.Step()
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(0xFFFD), cpu.Regs.PC)
|
||||||
|
}
|
||||||
45
gb/io.go
Normal file
45
gb/io.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package gb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SerialData [2]byte
|
||||||
|
var InterruptFlags byte
|
||||||
|
|
||||||
|
func IORead(address uint16) byte {
|
||||||
|
if address == 0xFF01 {
|
||||||
|
return SerialData[0]
|
||||||
|
} else if address == 0xFF02 {
|
||||||
|
return SerialData[1]
|
||||||
|
} else if (address >= 0xFF04) && (address <= 0xFF07) {
|
||||||
|
return timer.Read(address)
|
||||||
|
} else if address == 0xFF0F {
|
||||||
|
return InterruptFlags
|
||||||
|
} else if (address >= 0xFF10) && (address <= 0xFF3F) {
|
||||||
|
// Ignore sound
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Reading from IO: invalid address %X\n", address)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func IOWrite(address uint16, value byte) {
|
||||||
|
if address == 0xFF01 {
|
||||||
|
SerialData[0] = value
|
||||||
|
} else if address == 0xFF02 {
|
||||||
|
SerialData[1] = value
|
||||||
|
} else if (address >= 0xFF04) && (address <= 0xFF07) {
|
||||||
|
timer.Write(address, value)
|
||||||
|
} else if address == 0xFF0F {
|
||||||
|
InterruptFlags = value
|
||||||
|
} else if (address >= 0xFF10) && (address <= 0xFF3F) {
|
||||||
|
// Ignore sound
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Writing to IO: invalid address %X\n", address)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
55
gb/ram.go
Normal file
55
gb/ram.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package gb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RAM struct {
|
||||||
|
WRAM [0x2000]byte
|
||||||
|
HRAM [0x80]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRAM() *RAM {
|
||||||
|
ram := RAM{}
|
||||||
|
|
||||||
|
return &ram
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ram *RAM) WRAMRead(address uint16) byte {
|
||||||
|
// TODO(m): Understand this line
|
||||||
|
address -= 0xC000
|
||||||
|
|
||||||
|
if address >= 0x2000 {
|
||||||
|
fmt.Printf("Reading from WRAM: invalid address %X\n", address+0xC000)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ram.WRAM[address]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ram *RAM) WRAMWrite(address uint16, value byte) {
|
||||||
|
// TODO(m): Understand this line
|
||||||
|
address -= 0xC000
|
||||||
|
|
||||||
|
if address >= 0x2000 {
|
||||||
|
fmt.Printf("Writing to WRAM: invalid address %X\n", address)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ram.WRAM[address] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ram *RAM) HRAMRead(address uint16) byte {
|
||||||
|
// TODO(m): Understand this line
|
||||||
|
address -= 0xFF80
|
||||||
|
|
||||||
|
return ram.HRAM[address]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ram *RAM) HRAMWrite(address uint16, value byte) {
|
||||||
|
// TODO(m): Understand this line
|
||||||
|
address -= 0xFF80
|
||||||
|
|
||||||
|
ram.HRAM[address] = value
|
||||||
|
}
|
||||||
59
gb/stack_test.go
Normal file
59
gb/stack_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package gb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStackPush(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x00, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.StackPush(0xDE)
|
||||||
|
|
||||||
|
// Should decrement the stack pointer
|
||||||
|
assert.Equal(t, cpu.Regs.SP, uint16(0xDFFE))
|
||||||
|
|
||||||
|
// Should write the value to the stack
|
||||||
|
assert.Equal(t, cpu.Bus.Read(cpu.Regs.SP), byte(0xDE))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackPush16(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x00, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.StackPush16(0xDEAD)
|
||||||
|
|
||||||
|
// Should decrement the stack pointer twice
|
||||||
|
assert.Equal(t, cpu.Regs.SP, uint16(0xDFFD))
|
||||||
|
|
||||||
|
// Should write the value to the stack
|
||||||
|
assert.Equal(t, cpu.Bus.Read16(cpu.Regs.SP), uint16(0xDEAD))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackPop(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x00, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.StackPush(0xDE)
|
||||||
|
|
||||||
|
val := cpu.StackPop()
|
||||||
|
|
||||||
|
// Should increment the stack pointer
|
||||||
|
assert.Equal(t, cpu.Regs.SP, uint16(0xDFFF))
|
||||||
|
|
||||||
|
// Should return the byte value
|
||||||
|
assert.Equal(t, val, byte(0xDE))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackPop16(t *testing.T) {
|
||||||
|
cpu := createCPU([]byte{0x00, 0x00, 0x00})
|
||||||
|
|
||||||
|
cpu.StackPush16(0xBEEF)
|
||||||
|
|
||||||
|
val := cpu.StackPop16()
|
||||||
|
|
||||||
|
// Should increment the stack pointer
|
||||||
|
assert.Equal(t, cpu.Regs.SP, uint16(0xDFFF))
|
||||||
|
|
||||||
|
// Should return the 16 bit value
|
||||||
|
assert.Equal(t, val, uint16(0xBEEF))
|
||||||
|
}
|
||||||
125
gb/timer.go
Normal file
125
gb/timer.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package gb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var timer = Timer{DIV: 0xAC00}
|
||||||
|
|
||||||
|
type Timer struct {
|
||||||
|
DIV uint16
|
||||||
|
TIMA byte
|
||||||
|
TMA byte
|
||||||
|
TAC byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (timer *Timer) Tick() {
|
||||||
|
previousDIV := timer.DIV
|
||||||
|
timer.DIV++
|
||||||
|
|
||||||
|
timerNeedsUpdate := false
|
||||||
|
|
||||||
|
// Determine clock mode
|
||||||
|
// TAC (Timer control register)
|
||||||
|
// Bits 0-1 determines clock frequency
|
||||||
|
// 00: 4096 Hz
|
||||||
|
// 01: 262144 Hz
|
||||||
|
// 10: 65536 Hz
|
||||||
|
// 11: 16384 Hz
|
||||||
|
// Bit 2 determines whether timer is enabled or not
|
||||||
|
|
||||||
|
clockMode := timer.TAC & (0b11)
|
||||||
|
switch clockMode {
|
||||||
|
case 0b00:
|
||||||
|
// 4096 Hz
|
||||||
|
// Detect a falling edge by comparing bit 9 in the previous DIV value
|
||||||
|
// with bit 9 in the current DIV value
|
||||||
|
previouslyEnabled := previousDIV&(1<<9) != 0
|
||||||
|
currentlyDisabled := timer.DIV&(1<<9) == 0
|
||||||
|
timerNeedsUpdate = previouslyEnabled && currentlyDisabled
|
||||||
|
|
||||||
|
case 0b01:
|
||||||
|
// 262144 Hz
|
||||||
|
// Detect a falling edge by comparing bit 3 in the previous DIV value
|
||||||
|
// with bit 3 in the current DIV value
|
||||||
|
previouslyEnabled := previousDIV&(1<<3) != 0
|
||||||
|
currentlyDisabled := timer.DIV&(1<<3) == 0
|
||||||
|
timerNeedsUpdate = previouslyEnabled && currentlyDisabled
|
||||||
|
|
||||||
|
case 0b10:
|
||||||
|
// 65536 Hz
|
||||||
|
// Detect a falling edge by comparing bit 5 in the previous DIV value
|
||||||
|
// with bit 5 in the current DIV value
|
||||||
|
previouslyEnabled := previousDIV&(1<<5) != 0
|
||||||
|
currentlyDisabled := timer.DIV&(1<<5) == 0
|
||||||
|
timerNeedsUpdate = previouslyEnabled && currentlyDisabled
|
||||||
|
|
||||||
|
case 0b11:
|
||||||
|
// 16384 Hz
|
||||||
|
// Detect a falling edge by comparing bit 7 in the previous DIV value
|
||||||
|
// with bit 7 in the current DIV value
|
||||||
|
previouslyEnabled := previousDIV&(1<<7) != 0
|
||||||
|
currentlyDisabled := timer.DIV&(1<<7) == 0
|
||||||
|
timerNeedsUpdate = previouslyEnabled && currentlyDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
timerIsEnabled := timer.TAC&(1<<2) != 0
|
||||||
|
// If the timer needs to be updated based on the determined clock mode and the timer is enabled, increment the timer
|
||||||
|
if timerNeedsUpdate && timerIsEnabled {
|
||||||
|
timer.TIMA++
|
||||||
|
|
||||||
|
// Check if TIMA is going to wrap and trigger an interrupt if necessary
|
||||||
|
if timer.TIMA == 0xFF {
|
||||||
|
timer.TIMA = timer.TMA
|
||||||
|
|
||||||
|
fmt.Println("TODO: cpu_request_interrupt(IT_TIMER")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (timer *Timer) Read(address uint16) byte {
|
||||||
|
switch address {
|
||||||
|
case 0xFF04:
|
||||||
|
return byte(timer.DIV >> 8)
|
||||||
|
|
||||||
|
case 0xFF05:
|
||||||
|
return timer.TIMA
|
||||||
|
|
||||||
|
case 0xFF06:
|
||||||
|
return timer.TMA
|
||||||
|
|
||||||
|
case 0xFF07:
|
||||||
|
return timer.TAC
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("Reading from Timer: invalid address %X\n", address)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (timer *Timer) Write(address uint16, value byte) {
|
||||||
|
switch address {
|
||||||
|
case 0xFF04:
|
||||||
|
//DIV
|
||||||
|
timer.DIV = 0
|
||||||
|
|
||||||
|
case 0xFF05:
|
||||||
|
//TIMA
|
||||||
|
timer.TIMA = value
|
||||||
|
|
||||||
|
case 0xFF06:
|
||||||
|
//TMA
|
||||||
|
timer.TMA = value
|
||||||
|
|
||||||
|
case 0xFF07:
|
||||||
|
//TAC
|
||||||
|
timer.TAC = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("Writing to Timer: invalid address %X\n", address)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
main.go
30
main.go
@@ -1,16 +1,30 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gb-player/ui"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
// "gb-player/ui"
|
||||||
|
"gb-player/gb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// FIXME(m): Allow specifying rom file on command line
|
if len(os.Args) != 2 {
|
||||||
// if len(os.Args) != 2 {
|
log.Fatalln("No rom file specified")
|
||||||
// log.Fatalln("No rom file specified")
|
}
|
||||||
// }
|
romPath := os.Args[1]
|
||||||
// romPath := os.Args[1]
|
|
||||||
romPath := "rom.gb"
|
|
||||||
|
|
||||||
ui.Run(romPath)
|
// ui.Run(romPath)
|
||||||
|
|
||||||
|
console, err := gb.NewConsole(romPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Executing instructions")
|
||||||
|
|
||||||
|
for !console.CPU.Halted {
|
||||||
|
console.CPU.Step()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
roms/01-special.gb
Normal file
BIN
roms/01-special.gb
Normal file
Binary file not shown.
BIN
roms/02-interrupts.gb
Normal file
BIN
roms/02-interrupts.gb
Normal file
Binary file not shown.
BIN
roms/03-op sp,hl.gb
Normal file
BIN
roms/03-op sp,hl.gb
Normal file
Binary file not shown.
BIN
roms/04-op r,imm.gb
Normal file
BIN
roms/04-op r,imm.gb
Normal file
Binary file not shown.
BIN
roms/05-op rp.gb
Normal file
BIN
roms/05-op rp.gb
Normal file
Binary file not shown.
BIN
roms/06-ld r,r.gb
Normal file
BIN
roms/06-ld r,r.gb
Normal file
Binary file not shown.
BIN
roms/07-jr,jp,call,ret,rst.gb
Normal file
BIN
roms/07-jr,jp,call,ret,rst.gb
Normal file
Binary file not shown.
BIN
roms/08-misc instrs.gb
Normal file
BIN
roms/08-misc instrs.gb
Normal file
Binary file not shown.
BIN
roms/09-op r,r.gb
Normal file
BIN
roms/09-op r,r.gb
Normal file
Binary file not shown.
BIN
roms/10-bit ops.gb
Normal file
BIN
roms/10-bit ops.gb
Normal file
Binary file not shown.
BIN
roms/11-op a,(hl).gb
Normal file
BIN
roms/11-op a,(hl).gb
Normal file
Binary file not shown.
BIN
roms/cpu_instrs.gb
Normal file
BIN
roms/cpu_instrs.gb
Normal file
Binary file not shown.
BIN
roms/dmg-acid2.gb
Normal file
BIN
roms/dmg-acid2.gb
Normal file
Binary file not shown.
BIN
roms/failed-checksum.gb
Normal file
BIN
roms/failed-checksum.gb
Normal file
Binary file not shown.
BIN
roms/mem_timing.gb
Normal file
BIN
roms/mem_timing.gb
Normal file
Binary file not shown.
@@ -51,10 +51,15 @@ func (c *Controller) Run() {
|
|||||||
running := true
|
running := true
|
||||||
for running {
|
for running {
|
||||||
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
||||||
switch event.(type) {
|
switch e := event.(type) {
|
||||||
case *sdl.QuitEvent:
|
case *sdl.QuitEvent:
|
||||||
println("Quit")
|
|
||||||
running = false
|
running = false
|
||||||
|
case *sdl.KeyboardEvent:
|
||||||
|
if e.Type == sdl.KEYDOWN {
|
||||||
|
if e.Keysym.Sym == sdl.K_ESCAPE {
|
||||||
|
running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ func (view *View) Update(dt uint64) {
|
|||||||
console := view.console
|
console := view.console
|
||||||
renderer := view.controller.renderer
|
renderer := view.controller.renderer
|
||||||
|
|
||||||
// console.StepMilliSeconds(dt)
|
console.StepMilliSeconds(dt)
|
||||||
console.StepMilliSeconds(sdl.GetTicks64())
|
|
||||||
|
|
||||||
buffer := console.Buffer()
|
buffer := console.Buffer()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user