Gopher2600/hardware/memory/cartridge/cartridge.go
JetSetIlly da83fc311b removed complexity from cartridge fingerprinting process
all cartridge data is read through cartridgeloader io.Reader interface
2024-04-16 10:18:13 +01:00

575 lines
18 KiB
Go

// 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 <https://www.gnu.org/licenses/>.
package cartridge
import (
"errors"
"fmt"
"strings"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/coprocessor"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/ace"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/cdf"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/dpcplus"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/moviecart"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/plusrom"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/supercharger"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/resources/unique"
)
// Cartridge defines the information and operations for a VCS cartridge.
type Cartridge struct {
env *environment.Environment
// filename/hash taken from cartridgeloader. choosing not to keep a
// reference to the cartridge loader itself.
Filename string
ShortName string
Hash string
// the specific cartridge data, mapped appropriately to the memory
// interfaces
mapper mapper.CartMapper
// the CartBusStuff and CartCoProc interface are accessed a lot if
// available. rather than performing type assertions too frequently we do
// it in the Attach() function and the Plumb() function
hasBusStuff bool
busStuff mapper.CartBusStuff
hasCoProcBus bool
coprocBus coprocessor.CartCoProcBus
}
// sentinal error returned if operation is on the ejected cartridge type.
var Ejected = errors.New("cartridge ejected")
// NewCartridge is the preferred method of initialisation for the cartridge
// type.
func NewCartridge(env *environment.Environment) *Cartridge {
cart := &Cartridge{env: env}
cart.Eject()
return cart
}
// Snapshot creates a copy of the current cartridge.
func (cart *Cartridge) Snapshot() *Cartridge {
n := *cart
n.mapper = cart.mapper.Snapshot()
return &n
}
// Plumb makes sure everything is ship-shape after a rewind event.
//
// The fromDifferentEmulation indicates that the State has been created by a
// different VCS emulation than the one being plumbed into.
//
// See mapper.PlumbFromDifferentEmulation for how this affects mapper
// implementations.
func (cart *Cartridge) Plumb(env *environment.Environment, fromDifferentEmulation bool) {
cart.env = env
cart.busStuff, cart.hasBusStuff = cart.mapper.(mapper.CartBusStuff)
cart.coprocBus, cart.hasCoProcBus = cart.mapper.(coprocessor.CartCoProcBus)
if fromDifferentEmulation {
if m, ok := cart.mapper.(mapper.PlumbFromDifferentEmulation); ok {
m.PlumbFromDifferentEmulation(cart.env)
return
}
}
cart.mapper.Plumb(cart.env)
}
// Reset volative contents of Cartridge.
func (cart *Cartridge) Reset() {
cart.mapper.Reset()
}
// String returns a summary of the cartridge, it's mapper and any containers.
//
// For just the filename use the Filename field or the ShortName field.
// Filename includes the path.
//
// For just the mapping use the ID() function and for just the container ID use
// the ContainerID() function.
func (cart *Cartridge) String() string {
s := strings.Builder{}
s.WriteString(fmt.Sprintf("%s (%s)", cart.ShortName, cart.ID()))
if cc := cart.GetContainer(); cc != nil {
s.WriteString(fmt.Sprintf(" [%s]", cc.ContainerID()))
}
return s.String()
}
// MappedBanks returns a string summary of the mapping. ie. what banks are mapped in.
func (cart *Cartridge) MappedBanks() string {
return cart.mapper.MappedBanks()
}
// ID returns the cartridge mapping ID.
func (cart *Cartridge) ID() string {
return cart.mapper.ID()
}
// Container returns the cartridge continer ID. If the cartridge is not in a
// container the empty string is returned.
func (cart *Cartridge) ContainerID() string {
if cc := cart.GetContainer(); cc != nil {
return cc.ContainerID()
}
return ""
}
// Peek is an implementation of memory.DebugBus. Address must be normalised.
func (cart *Cartridge) Peek(addr uint16) (uint8, error) {
v, _, err := cart.mapper.Access(addr&memorymap.CartridgeBits, true)
return v, err
}
// Poke is an implementation of memory.DebugBus. Address must be normalised.
func (cart *Cartridge) Poke(addr uint16, data uint8) error {
return cart.mapper.AccessVolatile(addr&memorymap.CartridgeBits, data, true)
}
// Read is an implementation of memory.CPUBus.
func (cart *Cartridge) Read(addr uint16) (uint8, uint8, error) {
return cart.mapper.Access(addr&memorymap.CartridgeBits, false)
}
// Write is an implementation of memory.CPUBus.
func (cart *Cartridge) Write(addr uint16, data uint8) error {
return cart.mapper.AccessVolatile(addr&memorymap.CartridgeBits, data, false)
}
// Eject removes memory from cartridge space and unlike the real hardware,
// attaches a bank of empty memory - for convenience of the debugger.
func (cart *Cartridge) Eject() {
cart.Filename = "ejected"
cart.ShortName = "ejected"
cart.Hash = ""
cart.mapper = newEjected()
}
// IsEjected returns true if no cartridge is attached.
func (cart *Cartridge) IsEjected() bool {
_, ok := cart.mapper.(*ejected)
return ok
}
// Attach the cartridge loader to the VCS and make available the data to the CPU
// bus
//
// How cartridges are mapped into the VCS's 4k space can differs dramatically.
// Much of the implementation details have been cribbed from Kevin Horton's
// "Cart Information" document [sizes.txt]. Other sources of information noted
// as appropriate.
func (cart *Cartridge) Attach(cartload cartridgeloader.Loader) error {
var err error
cart.Filename = cartload.Filename
cart.ShortName = cartload.Name
cart.Hash = cartload.HashSHA1
cart.mapper = newEjected()
// reset loader stream before we go any further
err = cartload.Reset()
if err != nil {
return fmt.Errorf("cartridge: %w", err)
}
// log result of Attach() on function return
defer func() {
// we might have arrived here as a result of an error so we should
// check cart.mapper before trying to access it
if cart.mapper == nil {
return
}
// get busstuff and coproc interfaces
cart.busStuff, cart.hasBusStuff = cart.mapper.(mapper.CartBusStuff)
cart.coprocBus, cart.hasCoProcBus = cart.mapper.(coprocessor.CartCoProcBus)
if _, ok := cart.mapper.(*ejected); !ok {
logger.Logf("cartridge", "inserted %s", cart.mapper.ID())
}
}()
mapping := strings.ToUpper(cartload.Mapping)
// automatic fingerprinting of cartridge
if mapping == "" || mapping == "AUTO" {
mapping, err = cart.fingerprint(cartload)
if err != nil {
return fmt.Errorf("cartridge: %w", err)
}
// reset loader stream after fingerprinting
err = cartload.Reset()
if err != nil {
return fmt.Errorf("cartridge: %w", err)
}
}
// sometimes we force the presence of a superchip
forceSuperchip := false
switch mapping {
case "2K":
cart.mapper, err = newAtari2k(cart.env, cartload)
case "4K":
cart.mapper, err = newAtari4k(cart.env, cartload)
case "F8":
cart.mapper, err = newAtari8k(cart.env, cartload)
case "F6":
cart.mapper, err = newAtari16k(cart.env, cartload)
case "F4":
cart.mapper, err = newAtari32k(cart.env, cartload)
case "2KSC":
cart.mapper, err = newAtari2k(cart.env, cartload)
forceSuperchip = true
case "4KSC":
cart.mapper, err = newAtari4k(cart.env, cartload)
forceSuperchip = true
case "F8SC":
cart.mapper, err = newAtari8k(cart.env, cartload)
forceSuperchip = true
case "F6SC":
cart.mapper, err = newAtari16k(cart.env, cartload)
forceSuperchip = true
case "F4SC":
cart.mapper, err = newAtari32k(cart.env, cartload)
forceSuperchip = true
case "CV":
cart.mapper, err = newCommaVid(cart.env, cartload)
case "FA":
cart.mapper, err = newCBS(cart.env, cartload)
case "FE":
cart.mapper, err = newSCABS(cart.env, cartload)
case "E0":
cart.mapper, err = newParkerBros(cart.env, cartload)
case "E7":
cart.mapper, err = newMnetwork(cart.env, cartload)
case "3F":
cart.mapper, err = newTigervision(cart.env, cartload)
case "UA":
cart.mapper, err = newUA(cart.env, cartload)
case "AR":
cart.mapper, err = supercharger.NewSupercharger(cart.env, cartload)
case "DF":
cart.mapper, err = newDF(cart.env, cartload)
case "3E":
cart.mapper, err = new3e(cart.env, cartload)
case "E3P":
// synonym for 3E+
fallthrough
case "E3+":
// synonym for 3E+
fallthrough
case "3E+":
cart.mapper, err = new3ePlus(cart.env, cartload)
case "EF":
cart.mapper, err = newEF(cart.env, cartload)
case "EFSC":
cart.mapper, err = newEF(cart.env, cartload)
forceSuperchip = true
case "SB":
cart.mapper, err = newSuperbank(cart.env, cartload)
case "WD":
cart.mapper, err = newWicksteadDesign(cart.env, cartload)
case "ACE":
cart.mapper, err = ace.NewAce(cart.env, cartload)
case "DPC":
cart.mapper, err = newDPC(cart.env, cartload)
case "DPC+":
cart.mapper, err = dpcplus.NewDPCplus(cart.env, cartload)
case "CDF":
cart.mapper, err = cdf.NewCDF(cart.env, cartload, "CDFJ")
case "CDF0":
cart.mapper, err = cdf.NewCDF(cart.env, cartload, "CDF0")
case "CDF1":
cart.mapper, err = cdf.NewCDF(cart.env, cartload, "CDF1")
case "CDFJ":
cart.mapper, err = cdf.NewCDF(cart.env, cartload, "CDFJ")
case "CDFJ+":
cart.mapper, err = cdf.NewCDF(cart.env, cartload, "CDFJ+")
case "MVC":
cart.mapper, err = moviecart.NewMoviecart(cart.env, cartload)
}
if err != nil {
return fmt.Errorf("cartridge: %w", err)
}
// if the forceSuperchip flag has been raised or if cartridge mapper
// implements the optionalSuperChip interface then try to add the additional
// RAM
if forceSuperchip {
if superchip, ok := cart.mapper.(mapper.OptionalSuperchip); ok {
superchip.AddSuperchip(true)
} else {
logger.Logf("cartridge", "cannot add superchip to %s mapper", cart.ID())
}
} else if superchip, ok := cart.mapper.(mapper.OptionalSuperchip); ok {
superchip.AddSuperchip(false)
}
// if this is a moviecart cartridge then return now without checking for plusrom
if _, ok := cart.mapper.(*moviecart.Moviecart); ok {
return nil
}
// in addition to the regular fingerprint we also check to see if this
// is PlusROM cartridge (which can be combined with a regular cartridge
// format)
if cart.fingerprintPlusROM(cartload) {
plus, err := plusrom.NewPlusROM(cart.env, cart.mapper)
if err != nil {
if errors.Is(err, plusrom.NotAPlusROM) {
logger.Log("cartridge", err.Error())
return nil
}
if errors.Is(err, plusrom.CannotAdoptROM) {
logger.Log("cartridge", err.Error())
return nil
}
return fmt.Errorf("cartridge: %w", err)
}
logger.Logf("cartridge", "%s cartridge contained in PlusROM", cart.ID())
// we've wrapped the main cartridge mapper inside the PlusROM
// mapper and we need to point the mapper field to the the new
// PlusROM instance
cart.mapper = plus
}
return nil
}
// NumBanks returns the number of banks in the catridge.
func (cart *Cartridge) NumBanks() int {
return cart.mapper.NumBanks()
}
// GetBank returns the current bank information for the specified address. See
// documentation for memorymap.Bank for more information.
func (cart *Cartridge) GetBank(addr uint16) mapper.BankInfo {
bank := cart.mapper.GetBank(addr & memorymap.CartridgeBits)
bank.NonCart = addr&memorymap.OriginCart != memorymap.OriginCart
return bank
}
// AccessPassive is called so that the cartridge can respond to changes to the
// address and data bus even when the data bus is not addressed to the cartridge.
//
// The VCS cartridge port is wired up to all 13 address lines of the 6507.
// Under normal operation, the chip-select line is used by the cartridge to
// know when to put data on the data bus. If it's not "on" then the cartridge
// does nothing.
//
// However, regardless of the chip-select line, the address and data buses can
// be monitored for activity.
//
// Notably the tigervision (3F) mapper monitors and waits for address 0x003f,
// which is in the TIA address space. When this address is triggered, the
// tigervision cartridge will use whatever is on the data bus to switch banks.
//
// Similarly, the CBS (FA) mapper will switch banks on cartridge addresses 1ff8
// to 1ffa (and mirrors) but only if the data bus has the low bit set to one.
func (cart *Cartridge) AccessPassive(addr uint16, data uint8) error {
return cart.mapper.AccessPassive(addr, data)
}
// Step should be called every CPU cycle. The attached cartridge may or may not
// change its state as a result. In fact, very few cartridges care about this.
func (cart *Cartridge) Step(clock float32) {
cart.mapper.Step(clock)
}
// Hotload cartridge ROM into emulation. Not changing any other state of the
// emulation.
func (cart *Cartridge) HotLoad(cartload cartridgeloader.Loader) error {
if hl, ok := cart.mapper.(mapper.CartHotLoader); ok {
cart.Hash = cartload.HashSHA1
err := hl.HotLoad(cartload)
if err != nil {
return err
}
return nil
}
return fmt.Errorf("cartridge: %s does not support hotloading", cart.mapper.ID())
}
// GetRegistersBus returns interface to the registers of the cartridge or nil
// if cartridge has no registers.
func (cart *Cartridge) GetRegistersBus() mapper.CartRegistersBus {
if bus, ok := cart.mapper.(mapper.CartRegistersBus); ok {
return bus
}
return nil
}
// GetStaticBus returns interface to the static area of the cartridge or nil if
// cartridge has no static area.
func (cart *Cartridge) GetStaticBus() mapper.CartStaticBus {
if bus, ok := cart.mapper.(mapper.CartStaticBus); ok {
return bus
}
return nil
}
// GetRAMbus returns interface to ram busor nil if catridge contains no RAM.
func (cart *Cartridge) GetRAMbus() mapper.CartRAMbus {
if bus, ok := cart.mapper.(mapper.CartRAMbus); ok {
if bus.GetRAM() == nil {
return nil
}
return bus
}
return nil
}
// GetTapeBus returns interface to a tape bus or nil if catridge has no tape.
func (cart *Cartridge) GetTapeBus() mapper.CartTapeBus {
if bus, ok := cart.mapper.(mapper.CartTapeBus); ok {
return bus
}
return nil
}
// GetContainer returns interface to cartridge container or nil if cartridge is
// not in a container.
func (cart *Cartridge) GetContainer() mapper.CartContainer {
if cc, ok := cart.mapper.(mapper.CartContainer); ok {
return cc
}
return nil
}
// GetCartHotspots returns interface to hotspots bus or nil if cartridge has no
// hotspots it wants to report.
func (cart *Cartridge) GetCartLabelsBus() mapper.CartLabelsBus {
if cc, ok := cart.mapper.(mapper.CartLabelsBus); ok {
return cc
}
return nil
}
// GetCartHotspotsBus returns interface to hotspots bus or nil if cartridge has no
// hotspots it wants to report.
func (cart *Cartridge) GetCartHotspotsBus() mapper.CartHotspotsBus {
if cc, ok := cart.mapper.(mapper.CartHotspotsBus); ok {
return cc
}
return nil
}
// GetCoProcBus returns interface to the coprocessor interface or nil if no
// coprocessor is available on the cartridge.
func (cart *Cartridge) GetCoProcBus() coprocessor.CartCoProcBus {
if cart.hasCoProcBus {
return cart.coprocBus
}
return nil
}
// GetCoProc returns interface to the coprocessor interface or nil if no
// coprocessor is available on the cartridge.
func (cart *Cartridge) GetCoProc() coprocessor.CartCoProc {
if cart.hasCoProcBus {
return cart.coprocBus.GetCoProc()
}
return nil
}
func (cart *Cartridge) GetSuperchargerFastLoad() mapper.CartSuperChargerFastLoad {
if c, ok := cart.mapper.(mapper.CartSuperChargerFastLoad); ok {
return c
}
return nil
}
// CopyBanks returns the sequence of banks in a cartridge. To return the
// next bank in the sequence, call the function with the instance of
// mapper.BankContent returned from the previous call. The end of the sequence is
// indicated by the nil value. Start a new iteration with the nil argument.
func (cart *Cartridge) CopyBanks() ([]mapper.BankContent, error) {
return cart.mapper.CopyBanks(), nil
}
// RewindBoundary returns true if the cartridge indicates that something has
// happened that should not be part of the rewind history. Returns false if
// cartridge mapper does not care about the rewind sub-system.
func (cart *Cartridge) RewindBoundary() bool {
if rb, ok := cart.mapper.(mapper.CartRewindBoundary); ok {
return rb.RewindBoundary()
}
return false
}
// ROMDump implements the mapper.CartROMDump interface.
func (cart *Cartridge) ROMDump() (string, error) {
romdump := fmt.Sprintf("%s.bin", unique.Filename("", cart.ShortName))
if rb, ok := cart.mapper.(mapper.CartROMDump); ok {
return romdump, rb.ROMDump(romdump)
}
return "", fmt.Errorf("cartridge: %s does not support ROM dumping", cart.mapper.ID())
}
// SetYieldHook implements the coprocessor.CartCoProcBus interface.
func (cart *Cartridge) SetYieldHook(hook coprocessor.CartYieldHook) {
if cart.hasCoProcBus {
cart.coprocBus.SetYieldHook(hook)
}
}
// CoProcExecutionState implements the coprocessor.CartCoProcBus interface
//
// If cartridge does not have a coprocessor then an empty instance of
// mapper.CoProcExecutionState is returned
func (cart *Cartridge) CoProcExecutionState() coprocessor.CoProcExecutionState {
if cart.hasCoProcBus {
return cart.coprocBus.CoProcExecutionState()
}
return coprocessor.CoProcExecutionState{}
}
// BusStuff implements the mapper.CartBusStuff interface
func (cart *Cartridge) BusStuff() (uint8, bool) {
if cart.hasBusStuff {
return cart.busStuff.BusStuff()
}
return 0, false
}
// Patch implements the mapper.CartPatchable interface
func (cart *Cartridge) Patch(offset int, data uint8) error {
if cart, ok := cart.mapper.(mapper.CartPatchable); ok {
return cart.Patch(offset, data)
}
return fmt.Errorf("cartridge is not patchable")
}