// This file is part of Gopher2600. // // Gopher2600 is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Gopher2600 is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Gopher2600. If not, see . package cartridge import ( "fmt" "io" "math/bits" "os" "github.com/jetsetilly/gopher2600/cartridgeloader" "github.com/jetsetilly/gopher2600/environment" "github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper" "github.com/jetsetilly/gopher2600/hardware/memory/cpubus" "github.com/jetsetilly/gopher2600/hardware/memory/memorymap" "github.com/jetsetilly/gopher2600/logger" ) // from bankswitch_sizes.txt: // // 2K: // // -These carts are not bankswitched, however the data repeats twice in the // 4K address space. You'll need to manually double-up these images to 4K // if you want to put these in say, a 4K cart. // // 4K: // // -These images are not bankswitched. // // 8K: // // -F8: This is the 'standard' method to implement 8K carts. There are two // addresses which select between two unique 4K sections. They are 1FF8 // and 1FF9. Any access to either one of these locations switches banks. // Accessing 1FF8 switches in the first 4K, and accessing 1FF9 switches in // the last 4K. Note that you can only access one 4K at a time! // // 16K: // // -F6: The 'standard' method for implementing 16K of data. It is identical // to the F8 method above, except there are 4 4K banks. You select which // 4K bank by accessing 1FF6, 1FF7, 1FF8, and 1FF9. // // 32K: // // -F4: The 'standard' method for implementing 32K. Only one cart is known // to use it- Fatal Run. Like the F6 method, however there are 8 4K // banks instead of 4. You use 1FF4 to 1FFB to select the desired bank. // // // Some carts have extra RAM. The way of doing this for Atari format cartridges // is with the addition of a "superchip". // // Atari's 'Super Chip' is nothing more than a 128-byte RAM chip that maps // itsself in the first 256 bytes of cart memory. (1000-10FFh) The first 128 // bytes is the write port, while the second 128 bytes is the read port. The // difference in addresses is because there is no dedicated address line to the // cart to differentiate between read and write operations. // // Some people have experimented with 1k carts. These are treated like 2k carts. type atari struct { env *environment.Environment mappingID string // atari formats apart from 2k and 4k are divided into banks. 2k and 4k // ROMs conceptually have one bank bankSize int banks [][]uint8 // rewindable state state *atariState // binary file has empty area(s). we use this to decide whether // the cartridge RAM should be allocated // // the superchip is not added automatically needsSuperchip bool } // size of superchip RAM const superchipRAMsize = 128 // look for empty area (representing RAM) in binary data. func hasEmptyArea(d []uint8) bool { // if the first byte in the cartridge is repeated 'superchipRAMsize' times // then we deem it to be an empty area. // // for example: the Fatal Run (NTSC) ROM uses FF rather than 00 to fill the // empty space. b := d[0] for i := 1; i < superchipRAMsize; i++ { if d[i] != b { return false } } return true } // ROMDump implements the mapper.CartROMDump interface. func (cart *atari) ROMDump(filename string) error { f, err := os.Create(filename) if err != nil { return fmt.Errorf("%s: %w", cart.mappingID, err) } defer func() { err := f.Close() if err != nil { logger.Logf("%s", cart.mappingID, err.Error()) } }() for _, b := range cart.banks { _, err := f.Write(b) if err != nil { return fmt.Errorf("%s: %w", cart.mappingID, err) } } return nil } // MappedBanks implements the mapper.CartMapper interface. func (cart *atari) MappedBanks() string { return fmt.Sprintf("Bank: %d", cart.state.bank) } // ID implements the mapper.CartMapper interface. func (cart *atari) ID() string { return cart.mappingID } // reset is called by the Reset() function implemented in all child types of the // the atari type func (cart *atari) reset(numBanks int) { for i := range cart.state.ram { if cart.env.Prefs.RandomState.Get().(bool) { cart.state.ram[i] = uint8(cart.env.Random.NoRewind(0xff)) } else { cart.state.ram[i] = 0 } } if cart.env.Prefs.RandomState.Get().(bool) { cart.state.bank = cart.env.Random.NoRewind(numBanks) } else { // earlier revisions of this function handled the possibility of a different // required starting bank for specific cartridges. however I now believe that the // correct answer in all cases should be bank 0 // // in truth cartridges should be able to start in any bank and those don't // will have issues on the real hardware. those that require a specific // starting bank (eg. bank 3) will definitely have issues on real hardware // and is so outlandish we'll simply consider it to be wrong // // an example of a cartridge that definitely do need to start in bank 0 and // never any other is Berzerk (Voice Enhanced), an F6 type cartridge // // a counter-example of a cartridge that must start in a non-zero bank is an // early version of the pacman variation, Hack'em Hanglyman, which is an F8 // type cartridge and must start in bank 1 // // this last example is probably the cartridge that sent me down the wrong // path. despite being a favourite of mine, the ROM was never a commercial // release or even what might be called a "final" ROM cart.state.bank = 0 // however, I think we can do better. if we read the reset vector and // test whether the address pointed to is in cartridge space then we can // make a better guess at the correct starting bank if numBanks > 1 { for b := 0; b < numBanks; b++ { reset := cpubus.Reset & memorymap.CartridgeBits addr := uint16(cart.banks[b][reset]) | (uint16(cart.banks[b][reset+1]) << 8) if memorymap.IsArea(addr, memorymap.Cartridge) { // we also discount addresses that would not make sense even though they // are in cartridge space. for example anything after address 0xfffa // are the NMI, Reset and BRK vectors // // we could think about how the program might run into the end // of cartridge memory unless there is an intermediate jump // instruction but honestly that would be too complex and // unlikely to improve things // // (masking with CartridgeBits as normal to make sure we can // handle any cartridge mirror) if addr&memorymap.CartridgeBits < cpubus.NMI&memorymap.CartridgeBits { cart.state.bank = b break // for loop } } } } } } // GetBank implements the mapper.CartMapper interface. func (cart *atari) GetBank(addr uint16) mapper.BankInfo { // because atari bank switching swaps out the entire memory space, every // address points to whatever the current bank is. compare to parker bros. // cartridges. return mapper.BankInfo{Number: cart.state.bank, IsRAM: cart.state.ram != nil && addr >= 0x80 && addr <= 0xff} } // access implements the mapper.CartMapper interface. the bool return value is true if the address // has been recognised and serviced. func (cart *atari) access(addr uint16) (uint8, uint8, bool) { if cart.state.ram != nil { if addr <= 0x7f { return 0, 0, true } if addr >= 0x80 && addr <= 0xff { return cart.state.ram[addr-superchipRAMsize], mapper.CartDrivenPins, true } } return 0, mapper.CartDrivenPins, false } // accessVolatile implements the mapper.CartMapper interface. func (cart *atari) accessVolatile(addr uint16, data uint8, poke bool) error { if cart.state.ram != nil { if addr <= 0x7f { cart.state.ram[addr] = data return nil } } if poke { cart.banks[cart.state.bank][addr] = data return nil } return nil } // Patch implements the mapper.CartPatchable interface func (cart *atari) Patch(offset int, data uint8) error { if offset >= cart.bankSize*len(cart.banks) { return fmt.Errorf("%s: patch offset too high (%d)", cart.mappingID, offset) } bank := offset / cart.bankSize offset %= cart.bankSize cart.banks[bank][offset] = data return nil } // AccessPassive implements the mapper.CartMapper interface. func (cart *atari) AccessPassive(addr uint16, data uint8) error { return nil } // Step implements the mapper.CartMapper interface. func (cart *atari) Step(_ float32) { } // GetRAM implements the mapper.CartRAMBus interface. func (cart *atari) GetRAM() []mapper.CartRAM { if cart.state.ram == nil { return nil } r := make([]mapper.CartRAM, 1) r[0] = mapper.CartRAM{ Label: "Superchip", Origin: 0x1080, Data: make([]uint8, len(cart.state.ram)), Mapped: true, } copy(r[0].Data, cart.state.ram) return r } // PutRAM implements the mapper.CartRAMBus interface. func (cart *atari) PutRAM(_ int, idx int, data uint8) { if cart.state.ram == nil { return } cart.state.ram[idx] = data } // IterateBank implements the mapper.CartMapper interface. func (cart *atari) CopyBanks() []mapper.BankContent { c := make([]mapper.BankContent, len(cart.banks)) for b := 0; b < len(cart.banks); b++ { c[b] = mapper.BankContent{Number: b, Data: cart.banks[b], Origins: []uint16{memorymap.OriginCart}, } } return c } // AddSuperchip implements the mapper.OptionalSuperchip interface. func (cart *atari) AddSuperchip(force bool) { if force || cart.needsSuperchip { cart.mappingID = fmt.Sprintf("%s (SC)", cart.mappingID) cart.state.ram = make([]uint8, superchipRAMsize) } } // atari4k is the original and most straightforward format: // - Pitfall // - River Raid // - Barnstormer // - etc. type atari4k struct { atari } func newAtari4k(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) { data, err := io.ReadAll(loader) if err != nil { return nil, fmt.Errorf("4k: %w", err) } cart := &atari4k{ atari: atari{ env: env, bankSize: 4096, mappingID: "4k", banks: make([][]uint8, 1), state: newAtariState(), needsSuperchip: hasEmptyArea(data), }, } if len(data) != cart.bankSize*cart.NumBanks() { return nil, fmt.Errorf("4k: wrong number of bytes in the cartridge data") } cart.banks[0] = make([]uint8, cart.bankSize) copy(cart.banks[0], data) return cart, nil } // Snapshot implements the mapper.CartMapper interface. func (cart *atari4k) Snapshot() mapper.CartMapper { n := *cart n.state = cart.state.Snapshot() return &n } // Plumb implements the mapper.CartMapper interface. func (cart *atari4k) Plumb(env *environment.Environment) { cart.env = env } // Reset implements the mapper.CartMapper interface. func (cart *atari4k) Reset() { cart.reset(cart.NumBanks()) } // NumBanks implements the mapper.CartMapper interface. func (cart *atari4k) NumBanks() int { return 1 } // Access implements the mapper.CartMapper interface. func (cart *atari4k) Access(addr uint16, peek bool) (uint8, uint8, error) { if data, mask, ok := cart.atari.access(addr); ok { return data, mask, nil } return cart.banks[0][addr], mapper.CartDrivenPins, nil } // AccessVolatile implements the mapper.CartMapper interface. func (cart *atari4k) AccessVolatile(addr uint16, data uint8, poke bool) error { return cart.atari.accessVolatile(addr, data, poke) } // atari2k is the half-size cartridge of 2048 bytes: // - Combat // - Dragster // - Outlaw // - Surround // - early cartridges // // it also supports other cartridge sizes less than 4096 bytes type atari2k struct { atari mask uint16 } func newAtari2k(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) { data, err := io.ReadAll(loader) if err != nil { return nil, fmt.Errorf("2k: %w", err) } // support any size less than 4096 bytes that is a power of two if len(data) >= 4096 || bits.OnesCount(uint(len(data))) != 1 { return nil, fmt.Errorf("atari: unsupported cartridge size") } cart := &atari2k{ atari: atari{ env: env, bankSize: len(data), mappingID: "2k", banks: make([][]uint8, 1), needsSuperchip: hasEmptyArea(data), state: newAtariState(), }, mask: uint16(len(data) - 1), } cart.banks[0] = make([]uint8, cart.bankSize) copy(cart.banks[0], data) return cart, nil } // Snapshot implements the mapper.CartMapper interface. func (cart *atari2k) Snapshot() mapper.CartMapper { n := *cart n.state = cart.state.Snapshot() return &n } // Plumb implements the mapper.CartMapper interface. func (cart *atari2k) Plumb(env *environment.Environment) { cart.env = env } // Reset implements the mapper.CartMapper interface. func (cart *atari2k) Reset() { cart.reset(cart.NumBanks()) } // NumBanks implements the mapper.CartMapper interface. func (cart *atari2k) NumBanks() int { return 1 } // Access implements the mapper.CartMapper interface. func (cart *atari2k) Access(addr uint16, peek bool) (uint8, uint8, error) { if data, mask, ok := cart.atari.access(addr); ok { return data, mask, nil } return cart.banks[0][addr&cart.mask], mapper.CartDrivenPins, nil } // IterateBank implements the mapper.CartMapper interface. func (cart *atari2k) CopyBanks() []mapper.BankContent { c := make([]mapper.BankContent, 1) c[0] = mapper.BankContent{Number: 0, Data: cart.banks[0], Origins: []uint16{memorymap.OriginCart, memorymap.OriginCart + uint16(cart.bankSize)}, } return c } // AccessVolatile implements the mapper.CartMapper interface. func (cart *atari2k) AccessVolatile(addr uint16, data uint8, poke bool) error { return cart.atari.accessVolatile(addr, data, poke) } // atari8k (F8): // - ET // - Krull // - etc. type atari8k struct { atari } func newAtari8k(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) { data, err := io.ReadAll(loader) if err != nil { return nil, fmt.Errorf("F8: %w", err) } cart := &atari8k{ atari: atari{ env: env, bankSize: 4096, mappingID: "F8", needsSuperchip: hasEmptyArea(data), state: newAtariState(), }, } if len(data) != cart.bankSize*cart.NumBanks() { return nil, fmt.Errorf("F8: wrong number of bytes in the cartridge data") } cart.banks = make([][]uint8, cart.NumBanks()) for k := 0; k < cart.NumBanks(); k++ { cart.banks[k] = make([]uint8, cart.bankSize) offset := k * cart.bankSize copy(cart.banks[k], data[offset:offset+cart.bankSize]) } return cart, nil } // Snapshot implements the mapper.CartMapper interface. func (cart *atari8k) Snapshot() mapper.CartMapper { n := *cart n.state = cart.state.Snapshot() return &n } // Plumb implements the mapper.CartMapper interface. func (cart *atari8k) Plumb(env *environment.Environment) { cart.env = env } // Reset implements the mapper.CartMapper interface. func (cart *atari8k) Reset() { cart.reset(cart.NumBanks()) } // NumBanks implements the mapper.CartMapper interface. func (cart *atari8k) NumBanks() int { return 2 } // Access implements the mapper.CartMapper interface. func (cart *atari8k) Access(addr uint16, peek bool) (uint8, uint8, error) { if data, mask, ok := cart.atari.access(addr); ok { return data, mask, nil } if !peek { cart.bankswitch(addr) } return cart.banks[cart.state.bank][addr], mapper.CartDrivenPins, nil } // AccessVolatile implements the mapper.CartMapper interface. func (cart *atari8k) AccessVolatile(addr uint16, data uint8, poke bool) error { if !poke { if cart.bankswitch(addr) { return nil } } return cart.atari.accessVolatile(addr, data, poke) } // bankswitch on hotspot access. func (cart *atari8k) bankswitch(addr uint16) bool { if addr >= 0x0ff8 && addr <= 0x0ff9 { if addr == 0x0ff8 { cart.state.bank = 0 } else if addr == 0x0ff9 { cart.state.bank = 1 } return true } return false } // ReadHotspots implements the mapper.CartHotspotsBus interface. func (cart *atari8k) ReadHotspots() map[uint16]mapper.CartHotspotInfo { return map[uint16]mapper.CartHotspotInfo{ 0x1ff8: {Symbol: "BANK0", Action: mapper.HotspotBankSwitch}, 0x1ff9: {Symbol: "BANK1", Action: mapper.HotspotBankSwitch}, } } // WriteHotspots implements the mapper.CartHotspotsBus interface. func (cart *atari8k) WriteHotspots() map[uint16]mapper.CartHotspotInfo { return cart.ReadHotspots() } // atari16k (F6): // - Crystal Castle // - RS Boxing // - Midnite Magic // - etc. type atari16k struct { atari } func newAtari16k(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) { data, err := io.ReadAll(loader) if err != nil { return nil, fmt.Errorf("F6: %w", err) } cart := &atari16k{ atari: atari{ env: env, bankSize: 4096, mappingID: "F6", needsSuperchip: hasEmptyArea(data), state: newAtariState(), }, } if len(data) != cart.bankSize*cart.NumBanks() { return nil, fmt.Errorf("F6: wrong number of bytes in the cartridge data") } cart.banks = make([][]uint8, cart.NumBanks()) for k := 0; k < cart.NumBanks(); k++ { cart.banks[k] = make([]uint8, cart.bankSize) offset := k * cart.bankSize copy(cart.banks[k], data[offset:offset+cart.bankSize]) } return cart, nil } // Snapshot implements the mapper.CartMapper interface. func (cart *atari16k) Snapshot() mapper.CartMapper { n := *cart n.state = cart.state.Snapshot() return &n } // Plumb implements the mapper.CartMapper interface. func (cart *atari16k) Plumb(env *environment.Environment) { cart.env = env } // Reset implements the mapper.CartMapper interface. func (cart *atari16k) Reset() { cart.reset(cart.NumBanks()) } // NumBanks implements the mapper.CartMapper interface. func (cart *atari16k) NumBanks() int { return 4 } // Access implements the mapper.CartMapper interface. func (cart *atari16k) Access(addr uint16, peek bool) (uint8, uint8, error) { if data, mask, ok := cart.atari.access(addr); ok { return data, mask, nil } if !peek { cart.bankswitch(addr) } return cart.banks[cart.state.bank][addr], mapper.CartDrivenPins, nil } // AccessVolatile implements the mapper.CartMapper interface. func (cart *atari16k) AccessVolatile(addr uint16, data uint8, poke bool) error { if !poke { if cart.bankswitch(addr) { return nil } } return cart.atari.accessVolatile(addr, data, poke) } // bankswitch on hotspot access. func (cart *atari16k) bankswitch(addr uint16) bool { if addr >= 0x0ff6 && addr <= 0x0ff9 { if addr == 0x0ff6 { cart.state.bank = 0 } else if addr == 0x0ff7 { cart.state.bank = 1 } else if addr == 0x0ff8 { cart.state.bank = 2 } else if addr == 0x0ff9 { cart.state.bank = 3 } return true } return false } // ReadHotspots implements the mapper.CartHotspotsBus interface. func (cart *atari16k) ReadHotspots() map[uint16]mapper.CartHotspotInfo { return map[uint16]mapper.CartHotspotInfo{ 0x1ff6: {Symbol: "BANK0", Action: mapper.HotspotBankSwitch}, 0x1ff7: {Symbol: "BANK1", Action: mapper.HotspotBankSwitch}, 0x1ff8: {Symbol: "BANK2", Action: mapper.HotspotBankSwitch}, 0x1ff9: {Symbol: "BANK3", Action: mapper.HotspotBankSwitch}, } } // WriteHotspots implements the mapper.CartHotspotsBus interface. func (cart *atari16k) WriteHotspots() map[uint16]mapper.CartHotspotInfo { return cart.ReadHotspots() } // atari32k (F8) // - Fatal Run // - Super Mario Bros. // - Donkey Kong (homebrew) // - etc. type atari32k struct { atari } func newAtari32k(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) { data, err := io.ReadAll(loader) if err != nil { return nil, fmt.Errorf("F4: %w", err) } cart := &atari32k{ atari: atari{ env: env, bankSize: 4096, mappingID: "F4", needsSuperchip: hasEmptyArea(data), state: newAtariState(), }, } if len(data) != cart.bankSize*cart.NumBanks() { return nil, fmt.Errorf("F4: wrong number of bytes in the cartridge data") } cart.banks = make([][]uint8, cart.NumBanks()) for k := 0; k < cart.NumBanks(); k++ { cart.banks[k] = make([]uint8, cart.bankSize) offset := k * cart.bankSize copy(cart.banks[k], data[offset:offset+cart.bankSize]) } return cart, nil } // Snapshot implements the mapper.CartMapper interface. func (cart *atari32k) Snapshot() mapper.CartMapper { n := *cart n.state = cart.state.Snapshot() return &n } // Plumb implements the mapper.CartMapper interface. func (cart *atari32k) Plumb(env *environment.Environment) { cart.env = env } // Reset implements the mapper.CartMapper interface. func (cart *atari32k) Reset() { cart.reset(cart.NumBanks()) } // NumBanks implements the mapper.CartMapper interface. func (cart *atari32k) NumBanks() int { return 8 } // Access implements the mapper.CartMapper interface. func (cart *atari32k) Access(addr uint16, peek bool) (uint8, uint8, error) { if data, mask, ok := cart.atari.access(addr); ok { return data, mask, nil } if !peek { cart.bankswitch(addr) } return cart.banks[cart.state.bank][addr], mapper.CartDrivenPins, nil } // AccessVolatile implements the mapper.CartMapper interface. func (cart *atari32k) AccessVolatile(addr uint16, data uint8, poke bool) error { if !poke { if cart.bankswitch(addr) { return nil } } return cart.atari.accessVolatile(addr, data, poke) } // bankswitch on hotspot access. func (cart *atari32k) bankswitch(addr uint16) bool { if addr >= 0x0ff4 && addr <= 0xffb { if addr == 0x0ff4 { cart.state.bank = 0 } else if addr == 0x0ff5 { cart.state.bank = 1 } else if addr == 0x0ff6 { cart.state.bank = 2 } else if addr == 0x0ff7 { cart.state.bank = 3 } else if addr == 0x0ff8 { cart.state.bank = 4 } else if addr == 0x0ff9 { cart.state.bank = 5 } else if addr == 0x0ffa { cart.state.bank = 6 } else if addr == 0x0ffb { cart.state.bank = 7 } return true } return false } // ReadHotspots implements the mapper.CartHotspotsBus interface. func (cart *atari32k) ReadHotspots() map[uint16]mapper.CartHotspotInfo { return map[uint16]mapper.CartHotspotInfo{ 0x1ff4: {Symbol: "BANK0", Action: mapper.HotspotBankSwitch}, 0x1ff5: {Symbol: "BANK1", Action: mapper.HotspotBankSwitch}, 0x1ff6: {Symbol: "BANK2", Action: mapper.HotspotBankSwitch}, 0x1ff7: {Symbol: "BANK3", Action: mapper.HotspotBankSwitch}, 0x1ff8: {Symbol: "BANK4", Action: mapper.HotspotBankSwitch}, 0x1ff9: {Symbol: "BANK5", Action: mapper.HotspotBankSwitch}, 0x1ffa: {Symbol: "BANK6", Action: mapper.HotspotBankSwitch}, 0x1ffb: {Symbol: "BANK7", Action: mapper.HotspotBankSwitch}, } } // WriteHotspots implements the mapper.CartHotspotsBus interface. func (cart *atari32k) WriteHotspots() map[uint16]mapper.CartHotspotInfo { return cart.ReadHotspots() } // rewindable state for all atari cartridges. type atariState struct { // identifies the currently selected bank bank int // some atari ROMs support aditional RAM. this is sometimes referred to as // the superchip. ram is only added when it is detected ram []uint8 } func newAtariState() *atariState { // not allocating memory for RAM because some atari carts don't have it. // memory allocated in addSuperChip() if required return &atariState{} } // Snapshot implements the mapper.CartMapper interface. func (s *atariState) Snapshot() *atariState { n := *s if s.ram != nil { n.ram = make([]uint8, len(s.ram)) copy(n.ram, s.ram) } return &n }