Gopher2600/hardware/memory/cartridge/mapper_3eplus.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

357 lines
9.2 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 (
"fmt"
"io"
"strings"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
)
type m3ePlus struct {
env *environment.Environment
mappingID string
// 3e+ cartridge memory is segmented
bankSize int
banks [][]uint8
// rewindable state
state *m3ePlusState
// !!TODO: hotspot info for 3e+
}
// should work with any size cartridge that is a multiple of 1024:
//
// - tested with chess3E+20200519_3PQ6_SQ.bin
// https://atariage.com/forums/topic/299157-chess/?do=findComment&comment=4541517
//
// - specifciation:
// https://atariage.com/forums/topic/307914-3e-and-macros-are-your-friend/?tab=comments#comment-4561287
//
// cartridges:
//
// - chess (Andrew Davie)
func new3ePlus(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) {
data, err := io.ReadAll(loader)
if err != nil {
return nil, fmt.Errorf("3E+: %w", err)
}
cart := &m3ePlus{
env: env,
mappingID: "3E+",
bankSize: 1024,
state: newM3ePlusState(),
}
if len(data)%cart.bankSize != 0 {
return nil, fmt.Errorf("3E+: wrong number of bytes in the cartridge file")
}
numBanks := len(data) / cart.bankSize
cart.banks = make([][]uint8, numBanks)
// partition binary
for k := 0; k < 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
}
// MappedBanks implements the mapper.CartMapper interface.
func (cart *m3ePlus) MappedBanks() string {
s := strings.Builder{}
s.WriteString("segments: ")
for i := range cart.state.segment {
s.WriteString(fmt.Sprintf("%d", cart.state.segment[i]))
if cart.state.segmentIsRAM[i] {
s.WriteString("R ")
} else {
s.WriteString(" ")
}
}
return strings.TrimSpace(s.String())
}
// ID implements the mapper.CartMapper interface.
func (cart *m3ePlus) ID() string {
return cart.mappingID
}
// Snapshot implements the mapper.CartMapper interface.
func (cart *m3ePlus) Snapshot() mapper.CartMapper {
n := *cart
n.state = cart.state.Snapshot()
return &n
}
// Plumb implements the mapper.CartMapper interface.
func (cart *m3ePlus) Plumb(env *environment.Environment) {
cart.env = env
}
// Reset implements the mapper.CartMapper interface.
func (cart *m3ePlus) Reset() {
for b := range cart.state.ram {
for i := range cart.state.ram[b] {
if cart.env.Prefs.RandomState.Get().(bool) {
cart.state.ram[b][i] = uint8(cart.env.Random.NoRewind(0xff))
} else {
cart.state.ram[b][i] = 0
}
}
}
// The last 1K ROM ($FC00-$FFFF) segment in the 6502 address space (ie: $1C00-$1FFF)
// is initialised to point to the FIRST 1K of the ROM image, so the reset vectors
// must be placed at the end of the first 1K in the ROM image.
for i := range cart.state.segment {
cart.state.segment[i] = 0
cart.state.segmentIsRAM[i] = false
}
}
// Access implements the mapper.CartMapper interface.
func (cart *m3ePlus) Access(addr uint16, _ bool) (uint8, uint8, error) {
var segment int
if addr >= 0x0000 && addr <= 0x03ff {
segment = 0
} else if addr >= 0x0400 && addr <= 0x07ff {
segment = 1
} else if addr >= 0x0800 && addr <= 0x0bff {
segment = 2
} else if addr >= 0x0c00 && addr <= 0x0fff {
segment = 3
}
var data uint8
if cart.state.segmentIsRAM[segment] {
data = cart.state.ram[cart.state.segment[segment]][addr&0x01ff]
} else {
bank := cart.state.segment[segment]
if bank < len(cart.banks) {
data = cart.banks[bank][addr&0x03ff]
}
}
return data, mapper.CartDrivenPins, nil
}
// AccessVolatile implements the mapper.CartMapper interface.
func (cart *m3ePlus) AccessVolatile(addr uint16, data uint8, poke bool) error {
var segment int
if addr >= 0x0000 && addr <= 0x03ff {
segment = 0
} else if addr >= 0x0400 && addr <= 0x07ff {
segment = 1
} else if addr >= 0x0800 && addr <= 0x0bff {
segment = 2
} else if addr >= 0x0c00 && addr <= 0x0fff {
segment = 3
}
if cart.state.segmentIsRAM[segment] == false {
if poke {
bank := cart.state.segment[segment]
cart.banks[bank][addr&0x03ff] = data
}
return nil
}
bank := cart.state.segment[segment]
if addr >= uint16(0x0200+(0x400*segment)) {
cart.state.ram[bank][addr&0x01ff] = data
}
return nil
}
// NumBanks implements the mapper.CartMapper interface.
func (cart *m3ePlus) NumBanks() int {
return len(cart.banks)
}
// GetBank implements the mapper.CartMapper interface.
func (cart *m3ePlus) GetBank(addr uint16) mapper.BankInfo {
var seg int
if addr >= 0x0000 && addr <= 0x03ff {
seg = 0
} else if addr >= 0x0400 && addr <= 0x07ff {
seg = 1
} else if addr >= 0x0800 && addr <= 0x0bff {
seg = 2
} else { // remaining address is between 0x0c00 and 0x0fff
seg = 3
}
if cart.state.segmentIsRAM[seg] {
return mapper.BankInfo{Number: cart.state.segment[seg], IsRAM: true, IsSegmented: true, Segment: seg}
}
return mapper.BankInfo{Number: cart.state.segment[seg], IsSegmented: true, Segment: seg}
}
// Patch implements the mapper.CartPatchable interface
func (cart *m3ePlus) Patch(offset int, data uint8) error {
if offset >= cart.bankSize*len(cart.banks) {
return fmt.Errorf("3E+: patch offset too high (%d)", offset)
}
bank := offset / cart.bankSize
offset %= cart.bankSize
cart.banks[bank][offset] = data
return nil
}
// AccessPassive implements the mapper.CartMapper interface.
func (cart *m3ePlus) AccessPassive(addr uint16, data uint8) error {
// mapper 3e+ is a derivative of tigervision and so uses the same
// AccessPassive() mechanism. see the tigervision commentary for details
// bankswitch on hotspot access
if addr == 0x3f {
segment := data >> 6
romBank := (data & 0x3f) % uint8(cart.NumBanks())
cart.state.segment[segment] = int(romBank)
cart.state.segmentIsRAM[segment] = false
} else if addr == 0x3e {
segment := data >> 6
ramBank := (data & 0x3f) % uint8(len(cart.state.ram))
cart.state.segment[segment] = int(ramBank)
cart.state.segmentIsRAM[segment] = true
}
return nil
}
// Step implements the mapper.CartMapper interface.
func (cart *m3ePlus) Step(_ float32) {
}
// GetRAM implements the mapper.CartRAMBus interface.
func (cart *m3ePlus) GetRAM() []mapper.CartRAM {
r := make([]mapper.CartRAM, len(cart.state.ram))
for i := range cart.state.ram {
mapped := false
origin := uint16(0x0000)
for s := range cart.state.segment {
mapped = cart.state.segment[s] == i && cart.state.segmentIsRAM[s]
if mapped {
switch s {
case 0:
origin = uint16(0x1000)
case 1:
origin = uint16(0x1400)
case 2:
origin = uint16(0x1800)
case 3:
origin = uint16(0x1c00)
}
break // for loop
}
}
r[i] = mapper.CartRAM{
Label: fmt.Sprintf("%d", i),
Origin: origin,
Data: make([]uint8, len(cart.state.ram[i])),
Mapped: mapped,
}
copy(r[i].Data, cart.state.ram[i])
}
return r
}
// PutRAM implements the mapper.CartRAMBus interface.
func (cart *m3ePlus) PutRAM(bank int, idx int, data uint8) {
cart.state.ram[bank][idx] = data
}
// IterateBank implements the mapper.CartMapper interface.
func (cart *m3ePlus) 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,
memorymap.OriginCart + uint16(cart.bankSize),
memorymap.OriginCart + uint16(cart.bankSize)*2,
memorymap.OriginCart + uint16(cart.bankSize)*3},
}
}
return c
}
// rewindable state for the 3e+ cartridge.
type m3ePlusState struct {
// 64 is the maximum number of banks possible under the 3e+ scheme
ram [64][]uint8
// cartridge memory is segmented in the 3e+ format. the 3e+ documentation
// refers to these as slots. we prefer the segment terminology for
// consistency.
//
// hotspots are provided by the AccessPassive() function
segment [4]int
segmentIsRAM [4]bool
}
func newM3ePlusState() *m3ePlusState {
s := &m3ePlusState{}
// a ram bank is half the size of the available bank size. this is because
// of how ram is accessed - half the addresses are for reading and the
// other half are for writing.
const ramSize = 512
// allocate ram
for k := 0; k < len(s.ram); k++ {
s.ram[k] = make([]uint8, ramSize)
}
return s
}
// Snapshot implements the mapper.CartMapper interface.
func (s *m3ePlusState) Snapshot() *m3ePlusState {
n := *s
for k := 0; k < len(s.ram); k++ {
n.ram[k] = make([]uint8, len(s.ram[k]))
copy(n.ram[k], s.ram[k])
}
return &n
}