Support for state snapshots. F7 to save, F8 to load

This commit is contained in:
Ivan Izaguirre 2019-05-17 23:28:20 +02:00
parent cccb0583db
commit cb21a1fefc
14 changed files with 399 additions and 149 deletions

View File

@ -12,15 +12,14 @@ type Apple2 struct {
mmu *memoryManager
io *ioC0Page
cg *CharacterGenerator
cards []cardBase
isApple2e bool
panicSS bool
activeSlot int // Slot that has the addressing 0xc800 to 0ccfff
commandChannel chan int
cycleDurationNs float64 // Inverse of the cpu clock in Ghz
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
isColor bool
fastMode bool
fastRequestsCounter int
persistance *persistance
}
const (
@ -29,114 +28,6 @@ const (
cpuClockEuroMhz = 14.238 / 14
)
// NewApple2 instantiates an apple2
func NewApple2(romFile string, charRomFile string, clockMhz float64,
isColor bool, fastMode bool, panicSS bool) *Apple2 {
var a Apple2
a.mmu = newMemoryManager(&a)
a.cpu = core6502.NewNMOS6502(a.mmu)
a.mmu.loadRom(romFile)
if charRomFile != "" {
a.cg = NewCharacterGenerator(charRomFile)
}
a.commandChannel = make(chan int, 100)
a.isColor = isColor
a.fastMode = fastMode
a.panicSS = panicSS
if clockMhz <= 0 {
// Full speed
a.cycleDurationNs = 0
} else {
a.cycleDurationNs = 1000.0 / clockMhz
}
// Set the io in 0xc000
a.io = newIoC0Page(&a)
a.mmu.setPages(0xc0, 0xc0, a.io)
return &a
}
// AddDisk2 insterts a DiskII controller
func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) {
d := newCardDisk2(diskRomFile)
d.cardBase.insert(a, slot)
if diskImage != "" {
diskette := loadDisquette(diskImage)
//diskette.saveNib(diskImage + "bak")
d.drive[0].insertDiskette(diskette)
}
}
// AddLanguageCard inserts a 16Kb card
func (a *Apple2) AddLanguageCard(slot int) {
d := newCardLanguage()
d.cardBase.insert(a, slot)
d.applyState()
}
// AddSaturnCard inserts a 128Kb card
func (a *Apple2) AddSaturnCard(slot int) {
d := newCardSaturn()
d.cardBase.insert(a, slot)
d.applyState()
}
// ConfigureStdConsole uses stdin and stdout to interface with the Apple2
func (a *Apple2) ConfigureStdConsole(stdinKeyboard bool, stdoutScreen bool) {
if !stdinKeyboard && !stdoutScreen {
return
}
// Init frontend
fe := newAnsiConsoleFrontend(a, stdinKeyboard)
if stdinKeyboard {
a.io.setKeyboardProvider(fe)
}
if stdoutScreen {
go fe.textModeGoRoutine()
}
}
// SetKeyboardProvider attaches an external keyboard provider
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
a.io.setKeyboardProvider(kb)
}
// SetSpeakerProvider attaches an external keyboard provider
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
a.io.setSpeakerProvider(s)
}
const (
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
CommandToggleSpeed = iota + 1
// CommandToggleColor toggles between NTSC color TV and Green phospor monitor
CommandToggleColor
)
// SendCommand enqueues a command to the emulator thread
func (a *Apple2) SendCommand(command int) {
a.commandChannel <- command
}
func (a *Apple2) executeCommand(command int) {
switch command {
case CommandToggleSpeed:
if a.cycleDurationNs == 0 {
fmt.Println("Slow")
a.cycleDurationNs = 1000.0 / CpuClockMhz
} else {
fmt.Println("Fast")
a.cycleDurationNs = 0
}
case CommandToggleColor:
a.isColor = !a.isColor
}
}
const maxWaitDuration = 100 * time.Millisecond
// Run starts the Apple2 emulation
@ -164,8 +55,8 @@ func (a *Apple2) Run(log bool) {
clockDuration := time.Since(referenceTime)
simulatedDuration := time.Duration(float64(a.cpu.GetCycles()) * a.cycleDurationNs)
waitDuration := simulatedDuration - clockDuration
if waitDuration > maxWaitDuration {
// We have to wait too long. Let's fast forward
if waitDuration > maxWaitDuration || -waitDuration > maxWaitDuration {
// We have to wait too long or are too much behind. Let's fast forward
referenceTime = referenceTime.Add(-waitDuration)
waitDuration = 0
}
@ -176,12 +67,43 @@ func (a *Apple2) Run(log bool) {
}
}
// LoadRom loads a binary file to the top of the memory.
const (
apple2RomSize = 12 * 1024
apple2eRomSize = 16 * 1024
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
CommandToggleSpeed = iota + 1
// CommandToggleColor toggles between NTSC color TV and Green phospor monitor
CommandToggleColor
// CommandSaveState stores the state to file
CommandSaveState
// CommandLoadState reload the last state
CommandLoadState
)
// SendCommand enqueues a command to the emulator thread
func (a *Apple2) SendCommand(command int) {
a.commandChannel <- command
}
func (a *Apple2) executeCommand(command int) {
switch command {
case CommandToggleSpeed:
if a.cycleDurationNs == 0 {
fmt.Println("Slow")
a.cycleDurationNs = 1000.0 / CpuClockMhz
} else {
fmt.Println("Fast")
a.cycleDurationNs = 0
}
case CommandToggleColor:
a.isColor = !a.isColor
case CommandSaveState:
fmt.Println("Saving state")
a.persistance.save("apple2.state")
case CommandLoadState:
fmt.Println("Loading state")
a.persistance.load("apple2.state")
}
}
func (a *Apple2) requestFastMode() {
// Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain.
if a.fastMode {

91
apple2/apple2Setup.go Normal file
View File

@ -0,0 +1,91 @@
package apple2
import (
"go6502/core6502"
)
// NewApple2 instantiates an apple2
func NewApple2(romFile string, charRomFile string, clockMhz float64,
isColor bool, fastMode bool, panicSS bool) *Apple2 {
var a Apple2
a.persistance = newPersistance(&a)
a.mmu = newMemoryManager(&a, romFile)
a.persistance.register(a.mmu)
a.cpu = core6502.NewNMOS6502(a.mmu)
if charRomFile != "" {
a.cg = NewCharacterGenerator(charRomFile)
}
a.commandChannel = make(chan int, 100)
a.isColor = isColor
a.fastMode = fastMode
a.panicSS = panicSS
if clockMhz <= 0 {
// Full speed
a.cycleDurationNs = 0
} else {
a.cycleDurationNs = 1000.0 / clockMhz
}
// Set the io in 0xc000
a.io = newIoC0Page(&a)
a.persistance.register(a.io)
a.mmu.setPages(0xc0, 0xc0, a.io)
return &a
}
// AddDisk2 insterts a DiskII controller
func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) {
d := newCardDisk2(diskRomFile)
d.cardBase.insert(a, slot)
a.persistance.register(d)
if diskImage != "" {
diskette := loadDisquette(diskImage)
//diskette.saveNib(diskImage + "bak")
d.drive[0].insertDiskette(diskette)
}
}
// AddLanguageCard inserts a 16Kb card
func (a *Apple2) AddLanguageCard(slot int) {
d := newCardLanguage()
d.cardBase.insert(a, slot)
d.applyState()
a.persistance.register(d)
}
// AddSaturnCard inserts a 128Kb card
func (a *Apple2) AddSaturnCard(slot int) {
d := newCardSaturn()
d.cardBase.insert(a, slot)
d.applyState()
a.persistance.register(d)
}
// ConfigureStdConsole uses stdin and stdout to interface with the Apple2
func (a *Apple2) ConfigureStdConsole(stdinKeyboard bool, stdoutScreen bool) {
if !stdinKeyboard && !stdoutScreen {
return
}
// Init frontend
fe := newAnsiConsoleFrontend(a, stdinKeyboard)
if stdinKeyboard {
a.io.setKeyboardProvider(fe)
}
if stdoutScreen {
go fe.textModeGoRoutine()
}
}
// SetKeyboardProvider attaches an external keyboard provider
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
a.io.setKeyboardProvider(kb)
}
// SetSpeakerProvider attaches an external keyboard provider
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
a.io.setSpeakerProvider(s)
}

View File

@ -1,6 +1,10 @@
package apple2
import "io/ioutil"
import (
"encoding/binary"
"io"
"io/ioutil"
)
/*
https://applesaucefdc.com/woz/reference2/
@ -148,3 +152,31 @@ func loadCardRom(filename string) []uint8 {
func (d *cardDisk2Drive) insertDiskette(dt *diskette16sector) {
d.diskette = dt
}
func (c *cardDisk2) save(w io.Writer) {
binary.Write(w, binary.BigEndian, c.selected)
c.drive[0].save(w)
c.drive[1].save(w)
}
func (c *cardDisk2) load(r io.Reader) {
binary.Read(r, binary.BigEndian, &c.selected)
c.drive[0].load(r)
c.drive[1].load(r)
}
func (d *cardDisk2Drive) save(w io.Writer) {
binary.Write(w, binary.BigEndian, d.currentPhase)
binary.Write(w, binary.BigEndian, d.power)
binary.Write(w, binary.BigEndian, d.writeMode)
binary.Write(w, binary.BigEndian, d.halfTrack)
binary.Write(w, binary.BigEndian, d.position)
}
func (d *cardDisk2Drive) load(r io.Reader) {
binary.Read(r, binary.BigEndian, &d.currentPhase)
binary.Read(r, binary.BigEndian, &d.power)
binary.Read(r, binary.BigEndian, &d.writeMode)
binary.Read(r, binary.BigEndian, &d.halfTrack)
binary.Read(r, binary.BigEndian, &d.position)
}

View File

@ -1,5 +1,10 @@
package apple2
import (
"encoding/binary"
"io"
)
/*
Language card with 16 extra kb for the Apple ][ and ][+
Manual: http://www.applelogic.org/files/LANGCARDMAN.pdf
@ -118,3 +123,23 @@ func (c *cardLanguage) applyState() {
}
}
func (c *cardLanguage) save(w io.Writer) {
binary.Write(w, binary.BigEndian, c.readState)
binary.Write(w, binary.BigEndian, c.writeState)
binary.Write(w, binary.BigEndian, c.activeBank)
c.ramBankA.save(w)
c.ramBankB.save(w)
c.ramUpper.save(w)
}
func (c *cardLanguage) load(r io.Reader) {
binary.Read(r, binary.BigEndian, &c.readState)
binary.Read(r, binary.BigEndian, &c.writeState)
binary.Read(r, binary.BigEndian, &c.activeBank)
c.ramBankA.load(r)
c.ramBankB.load(r)
c.ramUpper.load(r)
c.applyState()
}

View File

@ -1,5 +1,10 @@
package apple2
import (
"encoding/binary"
"io"
)
/*
RAM card with 128Kb. It's like 8 language cards.
@ -14,9 +19,9 @@ type cardSaturn struct {
writeState int
activeBank int
activeBlock int
ramBankA [8]*memoryRange // First 4kb to map in 0xD000-0xDFFF
ramBankB [8]*memoryRange // Second 4kb to map in 0xD000-0xDFFF
ramUpper [8]*memoryRange // Upper 8kb to map in 0xE000-0xFFFF
ramBankA [saturnBlocks]*memoryRange // First 4kb to map in 0xD000-0xDFFF
ramBankB [saturnBlocks]*memoryRange // Second 4kb to map in 0xD000-0xDFFF
ramUpper [saturnBlocks]*memoryRange // Upper 8kb to map in 0xE000-0xFFFF
}
const (
@ -26,13 +31,17 @@ const (
saturnWriteEnabled = 2
)
const (
saturnBlocks = 8
)
func newCardSaturn() *cardSaturn {
var c cardSaturn
c.readState = false
c.writeState = lcWriteEnabled
c.activeBank = 1
for i := 0; i < 8; i++ {
for i := 0; i < saturnBlocks; i++ {
c.ramBankA[i] = newMemoryRange(0xd000, make([]uint8, 0x1000))
c.ramBankB[i] = newMemoryRange(0xd000, make([]uint8, 0x1000))
c.ramUpper[i] = newMemoryRange(0xe000, make([]uint8, 0x2000))
@ -102,13 +111,13 @@ func (c *cardSaturn) ssAction(ss int) {
c.readState = true
c.writeState++
case 12:
c.activeBlock = 0
c.activeBlock = 4
case 13:
c.activeBlock = 1
c.activeBlock = 5
case 14:
c.activeBlock = 2
c.activeBlock = 6
case 15:
c.activeBlock = 3
c.activeBlock = 7
}
if c.writeState > lcWriteEnabled {
@ -142,5 +151,30 @@ func (c *cardSaturn) applyState() {
} else {
mmu.setPagesWrite(0xd0, 0xff, nil)
}
}
func (c *cardSaturn) save(w io.Writer) {
for i := 0; i < saturnBlocks; i++ {
binary.Write(w, binary.BigEndian, c.readState)
binary.Write(w, binary.BigEndian, c.writeState)
binary.Write(w, binary.BigEndian, c.activeBank)
binary.Write(w, binary.BigEndian, c.activeBlock)
c.ramBankA[i].save(w)
c.ramBankB[i].save(w)
c.ramUpper[i].save(w)
}
}
func (c *cardSaturn) load(r io.Reader) {
for i := 0; i < saturnBlocks; i++ {
binary.Read(r, binary.BigEndian, &c.readState)
binary.Read(r, binary.BigEndian, &c.writeState)
binary.Read(r, binary.BigEndian, &c.activeBank)
binary.Read(r, binary.BigEndian, &c.activeBlock)
c.ramBankA[i].load(r)
c.ramBankB[i].load(r)
c.ramUpper[i].load(r)
c.applyState()
}
}

View File

@ -1,7 +1,9 @@
package apple2
import (
"encoding/binary"
"fmt"
"io"
)
type ioC0Page struct {
@ -47,6 +49,14 @@ func newIoC0Page(a *Apple2) *ioC0Page {
return &io
}
func (p *ioC0Page) save(w io.Writer) {
binary.Write(w, binary.BigEndian, p.softSwitchesData)
}
func (p *ioC0Page) load(r io.Reader) {
binary.Read(r, binary.BigEndian, &p.softSwitchesData)
}
func (p *ioC0Page) addSoftSwitchRW(address uint8, ss softSwitchR) {
p.addSoftSwitchR(address, ss)
p.addSoftSwitchW(address, func(p *ioC0Page, _ uint8) {

View File

@ -1,6 +1,7 @@
package apple2
import (
"io"
"io/ioutil"
)
@ -91,18 +92,35 @@ func (mmu *memoryManager) resetSlotExpansionRoms() {
mmu.setPagesRead(0xc8, 0xcf, nil)
}
func newMemoryManager(a *Apple2) *memoryManager {
func (mmu *memoryManager) resetRomPaging() {
// Assign the first 12kb of ROM from 0xd000 to 0xffff
mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM)
}
func (mmu *memoryManager) resetBaseRamPaging() {
// Assign the base RAM from 0x0000 to 0xbfff
mmu.setPages(0x00, 0xbf, mmu.physicalMainRAM)
}
func newMemoryManager(a *Apple2, romFile string) *memoryManager {
var mmu memoryManager
mmu.apple2 = a
// Assign RAM from 0x0000 to 0xbfff, 48kb
ram := make([]uint8, 0xc000)
ram := make([]uint8, 0xc000) // Reserve 48kb
mmu.physicalMainRAM = newMemoryRange(0, ram)
mmu.setPages(0x00, 0xc0, mmu.physicalMainRAM)
mmu.loadRom(romFile)
mmu.resetBaseRamPaging()
mmu.resetRomPaging()
return &mmu
}
const (
apple2RomSize = 12 * 1024
apple2eRomSize = 16 * 1024
)
func (mmu *memoryManager) loadRom(filename string) {
data, err := ioutil.ReadFile(filename)
if err != nil {
@ -120,17 +138,18 @@ func (mmu *memoryManager) loadRom(filename string) {
// It starts with 256 unused bytes not mapped to 0xc000.
a.isApple2e = true
extraRomSize := apple2eRomSize - apple2RomSize
a.mmu.physicalROMe = newMemoryRange(0xc000, data[0:extraRomSize])
mmu.physicalROMe = newMemoryRange(0xc000, data[0:extraRomSize])
romStart = extraRomSize
}
a.mmu.physicalROM = newMemoryRange(0xd000, data[romStart:])
mmu.resetRomPaging()
mmu.physicalROM = newMemoryRange(0xd000, data[romStart:])
}
func (mmu *memoryManager) resetRomPaging() {
// Assign the first 12kb of ROM from 0xd000 to 0xfff
for i := 0x0000; i < 0x3000; i = i + 0x100 {
mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM)
}
func (mmu *memoryManager) save(w io.Writer) {
mmu.physicalMainRAM.save(w)
}
func (mmu *memoryManager) load(r io.Reader) {
mmu.physicalMainRAM.load(r)
mmu.resetBaseRamPaging()
}

View File

@ -1,5 +1,10 @@
package apple2
import (
"encoding/binary"
"io"
)
type memoryRange struct {
base uint16
data []uint8
@ -23,3 +28,13 @@ func (m *memoryRange) poke(address uint16, value uint8) {
func (m *memoryRange) subRange(a, b uint16) []uint8 {
return m.data[a-m.base : b-m.base]
}
func (m *memoryRange) save(w io.Writer) {
binary.Write(w, binary.BigEndian, m.base)
binary.Write(w, binary.BigEndian, m.data)
}
func (m *memoryRange) load(r io.Reader) {
binary.Read(r, binary.BigEndian, &m.base)
binary.Read(r, binary.BigEndian, &m.data)
}

66
apple2/persistance.go Normal file
View File

@ -0,0 +1,66 @@
package apple2
import (
"bufio"
"encoding/binary"
"io"
"os"
)
type persistent interface {
save(w io.Writer)
load(r io.Reader)
}
type persistance struct {
a *Apple2
items []persistent
}
func newPersistance(a *Apple2) *persistance {
var p persistance
p.a = a
return &p
}
func (p *persistance) register(i persistent) {
p.items = append(p.items, i)
}
func (p *persistance) save(filename string) {
f, err := os.Create(filename)
if err != nil {
panic(err)
}
defer f.Close()
w := bufio.NewWriter(f)
defer w.Flush()
binary.Write(w, binary.BigEndian, p.a.isColor)
binary.Write(w, binary.BigEndian, p.a.fastMode)
binary.Write(w, binary.BigEndian, p.a.fastRequestsCounter)
p.a.cpu.Save(w)
for _, v := range p.items {
v.save(w)
}
}
func (p *persistance) load(filename string) {
f, err := os.Open(filename)
if err != nil {
// Ignore error if can't load the file
return
}
defer f.Close()
r := bufio.NewReader(f)
binary.Read(r, binary.BigEndian, &p.a.isColor)
binary.Read(r, binary.BigEndian, &p.a.fastMode)
binary.Read(r, binary.BigEndian, &p.a.fastRequestsCounter)
p.a.cpu.Load(r)
for _, v := range p.items {
v.load(r)
}
}

View File

@ -61,3 +61,5 @@ func softSwitchSlotC3RomOn(io *ioC0Page) {
func softSwitchSlotC3RomOff(io *ioC0Page) {
io.apple2.mmu.setPagesRead(0xc3, 0xc3, io.apple2.mmu.physicalROMe)
}
// TODO: apply state after persistance load

View File

@ -93,6 +93,10 @@ func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) {
k.a.SendCommand(apple2.CommandToggleSpeed)
case sdl.K_F6:
k.a.SendCommand(apple2.CommandToggleColor)
case sdl.K_F7:
k.a.SendCommand(apple2.CommandSaveState)
case sdl.K_F8:
k.a.SendCommand(apple2.CommandLoadState)
}
// Missing values 91 to 95. Usually control for [\]^_

View File

@ -1,6 +1,10 @@
package core6502
import "fmt"
import (
"encoding/binary"
"fmt"
"io"
)
// https://www.masswerk.at/6502/6502_instruction_set.html
// http://www.emulator101.com/reference/6502-reference.html
@ -78,6 +82,18 @@ func (s *State) GetCycles() uint64 {
return s.cycles
}
// Save saves the CPU state (registers and cycle counter)
func (s *State) Save(w io.Writer) {
binary.Write(w, binary.BigEndian, s.cycles)
binary.Write(w, binary.BigEndian, s.reg.data)
}
// Load loads the CPU state (registers and cycle counter)
func (s *State) Load(r io.Reader) {
binary.Read(r, binary.BigEndian, &s.cycles)
binary.Read(r, binary.BigEndian, &s.reg.data)
}
func lineString(line []uint8, opcode opcode) string {
t := opcode.name
switch opcode.addressMode {

View File

@ -6,9 +6,10 @@ const (
regA = 0
regX = 1
regY = 2
regP = 4
regSP = 5
regPC = 6 // 2 bytes
regP = 3
regSP = 4
regPC = 5 // 2 bytes
regPC2 = 6
regNone = -1
)
@ -24,7 +25,7 @@ const (
)
type registers struct {
data [8]uint8
data [7]uint8
}
func (r *registers) getRegister(i int) uint8 { return r.data[i] }
@ -45,12 +46,12 @@ func (r *registers) setP(v uint8) { r.setRegister(regP, v) }
func (r *registers) setSP(v uint8) { r.setRegister(regSP, v) }
func (r *registers) getPC() uint16 {
return uint16(r.data[regPC])*256 + uint16(r.data[regPC+1])
return uint16(r.data[regPC])*256 + uint16(r.data[regPC2])
}
func (r *registers) setPC(v uint16) {
r.data[regPC] = uint8(v >> 8)
r.data[regPC+1] = uint8(v)
r.data[regPC2] = uint8(v)
}
func (r *registers) getFlagBit(i uint8) uint8 {

21
main.go
View File

@ -18,7 +18,7 @@ func main() {
disk2Slot := flag.Int(
"disk2Slot",
6,
"slot for the disk driver. 0 for none.")
"slot for the disk driver. -1 for none.")
diskImage := flag.String(
"disk",
"../dos33.dsk",
@ -31,6 +31,15 @@ func main() {
"charRom",
"apple2/romdumps/Apple2rev7CharGen.rom",
"rom file for the disk drive controller")
languageCardSlot := flag.Int(
"languageCardSlot",
0,
"slot for the 16kb language card. -1 for none")
saturnCardSlot := flag.Int(
"saturnCardSlot",
-1,
"slot for the 256kb Saturn card. -1 for none")
useSdl := flag.Bool(
"sdl",
true,
@ -69,9 +78,13 @@ func main() {
log := false
a := apple2.NewApple2(*romFile, *charRomFile, *cpuClock, !*mono, *fastDisk, *panicSS)
//a.AddLanguageCard(0)
a.AddSaturnCard(0)
if *disk2Slot > 0 {
if *languageCardSlot >= 0 {
a.AddLanguageCard(*languageCardSlot)
}
if *saturnCardSlot >= 0 {
a.AddSaturnCard(*saturnCardSlot)
}
if *disk2Slot >= 0 {
a.AddDisk2(*disk2Slot, *disk2RomFile, *diskImage)
}
if *useSdl {