Memory manager rewritten. Logic calculated on every access.
This commit is contained in:
parent
3618cbb9c9
commit
3660e0ae98
|
@ -13,10 +13,7 @@ func newApple2plus() *Apple2 {
|
||||||
a.Name = "Apple ][+"
|
a.Name = "Apple ][+"
|
||||||
a.mmu = newMemoryManager(&a)
|
a.mmu = newMemoryManager(&a)
|
||||||
a.cpu = core6502.NewNMOS6502(a.mmu)
|
a.cpu = core6502.NewNMOS6502(a.mmu)
|
||||||
|
|
||||||
// Set the io in 0xc000
|
|
||||||
a.io = newIoC0Page(&a)
|
a.io = newIoC0Page(&a)
|
||||||
a.mmu.setPages(0xc0, 0xc0, a.io)
|
|
||||||
addApple2SoftSwitches(a.io)
|
addApple2SoftSwitches(a.io)
|
||||||
|
|
||||||
return &a
|
return &a
|
||||||
|
@ -28,10 +25,7 @@ func newApple2e() *Apple2 {
|
||||||
a.Name = "Apple IIe"
|
a.Name = "Apple IIe"
|
||||||
a.mmu = newMemoryManager(&a)
|
a.mmu = newMemoryManager(&a)
|
||||||
a.cpu = core6502.NewCMOS65c02(a.mmu)
|
a.cpu = core6502.NewCMOS65c02(a.mmu)
|
||||||
|
|
||||||
// Set the io in 0xc000
|
|
||||||
a.io = newIoC0Page(&a)
|
a.io = newIoC0Page(&a)
|
||||||
a.mmu.setPages(0xc0, 0xc0, a.io)
|
|
||||||
addApple2SoftSwitches(a.io)
|
addApple2SoftSwitches(a.io)
|
||||||
addApple2ESoftSwitches(a.io)
|
addApple2ESoftSwitches(a.io)
|
||||||
|
|
||||||
|
@ -44,10 +38,7 @@ func newApple2eEnhanced() *Apple2 {
|
||||||
a.Name = "Apple //e"
|
a.Name = "Apple //e"
|
||||||
a.mmu = newMemoryManager(&a)
|
a.mmu = newMemoryManager(&a)
|
||||||
a.cpu = core6502.NewCMOS65c02(a.mmu)
|
a.cpu = core6502.NewCMOS65c02(a.mmu)
|
||||||
|
|
||||||
// Set the io in 0xc000
|
|
||||||
a.io = newIoC0Page(&a)
|
a.io = newIoC0Page(&a)
|
||||||
a.mmu.setPages(0xc0, 0xc0, a.io)
|
|
||||||
addApple2SoftSwitches(a.io)
|
addApple2SoftSwitches(a.io)
|
||||||
addApple2ESoftSwitches(a.io)
|
addApple2ESoftSwitches(a.io)
|
||||||
|
|
||||||
|
@ -100,8 +91,7 @@ func (a *Apple2) LoadRom(filename string) error {
|
||||||
romStart = extraRomSize
|
romStart = extraRomSize
|
||||||
}
|
}
|
||||||
|
|
||||||
mmu.physicalROM = newMemoryRange(0xd000, data[romStart:])
|
mmu.physicalROM[0] = newMemoryRange(0xd000, data[romStart:])
|
||||||
mmu.resetRomPaging()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,7 @@ func newBase64a() *Apple2 {
|
||||||
a.Name = "Base 64A"
|
a.Name = "Base 64A"
|
||||||
a.mmu = newMemoryManager(&a)
|
a.mmu = newMemoryManager(&a)
|
||||||
a.cpu = core6502.NewNMOS6502(a.mmu)
|
a.cpu = core6502.NewNMOS6502(a.mmu)
|
||||||
|
|
||||||
// Set the io in 0xc000
|
|
||||||
a.io = newIoC0Page(&a)
|
a.io = newIoC0Page(&a)
|
||||||
a.mmu.setPages(0xc0, 0xc0, a.io)
|
|
||||||
addApple2SoftSwitches(a.io)
|
addApple2SoftSwitches(a.io)
|
||||||
addBase64aSoftSwitches(a.io)
|
addBase64aSoftSwitches(a.io)
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,6 @@ import (
|
||||||
Copam BASE64A uses paginated ROM
|
Copam BASE64A uses paginated ROM
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Base64aROM Models the paginated ROM on a BASE64A clone
|
|
||||||
type base64aROM struct {
|
|
||||||
romBanks [4]*memoryRange
|
|
||||||
romBank uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// There are 6 ROM chips. Each can have 4Kb or 8Kb. They can fill
|
// There are 6 ROM chips. Each can have 4Kb or 8Kb. They can fill
|
||||||
// 2 or 4 banks with 2kb windows.
|
// 2 or 4 banks with 2kb windows.
|
||||||
|
@ -43,38 +37,34 @@ func loadBase64aRom(a *Apple2) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create banks
|
// Create banks
|
||||||
var r base64aROM
|
|
||||||
for j := range romBanksBytes {
|
for j := range romBanksBytes {
|
||||||
r.romBanks[j] = newMemoryRange(0xd000, romBanksBytes[j])
|
a.mmu.physicalROM[j] = newMemoryRange(0xd000, romBanksBytes[j])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with first bank active
|
// Start with first bank active
|
||||||
r.changeBank(a.mmu, 0)
|
a.mmu.setActiveROMPage(0)
|
||||||
|
|
||||||
// Add rom soft switches. They use the annunciator 0 and 1
|
// Add rom soft switches. They use the annunciator 0 and 1
|
||||||
a.io.addSoftSwitchRW(0x58, func(*ioC0Page) uint8 {
|
a.io.addSoftSwitchRW(0x58, func(*ioC0Page) uint8 {
|
||||||
r.changeBank(a.mmu, r.romBank&2)
|
p := a.mmu.getActiveROMPage()
|
||||||
|
a.mmu.setActiveROMPage(p & 2)
|
||||||
return 0
|
return 0
|
||||||
}, "ANN0OFF-ROM")
|
}, "ANN0OFF-ROM")
|
||||||
a.io.addSoftSwitchRW(0x59, func(*ioC0Page) uint8 {
|
a.io.addSoftSwitchRW(0x59, func(*ioC0Page) uint8 {
|
||||||
r.changeBank(a.mmu, r.romBank|1)
|
p := a.mmu.getActiveROMPage()
|
||||||
|
a.mmu.setActiveROMPage(p | 1)
|
||||||
return 0
|
return 0
|
||||||
}, "ANN0ON-ROM")
|
}, "ANN0ON-ROM")
|
||||||
a.io.addSoftSwitchRW(0x5A, func(*ioC0Page) uint8 {
|
a.io.addSoftSwitchRW(0x5A, func(*ioC0Page) uint8 {
|
||||||
r.changeBank(a.mmu, r.romBank&1)
|
p := a.mmu.getActiveROMPage()
|
||||||
|
a.mmu.setActiveROMPage(p & 1)
|
||||||
return 0
|
return 0
|
||||||
}, "ANN1OFF-ROM")
|
}, "ANN1OFF-ROM")
|
||||||
a.io.addSoftSwitchRW(0x5B, func(*ioC0Page) uint8 {
|
a.io.addSoftSwitchRW(0x5B, func(*ioC0Page) uint8 {
|
||||||
r.changeBank(a.mmu, r.romBank|2)
|
p := a.mmu.getActiveROMPage()
|
||||||
|
a.mmu.setActiveROMPage(p | 2)
|
||||||
return 0
|
return 0
|
||||||
}, "ANN1ON-ROM")
|
}, "ANN1ON-ROM")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *base64aROM) changeBank(mmu *memoryManager, bank uint8) {
|
|
||||||
r.romBank = bank
|
|
||||||
//fmt.Printf("Change to ROM bank #%v\n", r.romBank)
|
|
||||||
mmu.physicalROM = r.romBanks[r.romBank]
|
|
||||||
mmu.resetRomPaging() // If rom was not active. This is going too far?
|
|
||||||
}
|
|
||||||
|
|
|
@ -38,12 +38,12 @@ func (c *cardBase) assign(a *Apple2, slot int) {
|
||||||
c.slot = slot
|
c.slot = slot
|
||||||
if slot != 0 && c.rom != nil {
|
if slot != 0 && c.rom != nil {
|
||||||
c.rom.base = uint16(0xc000 + slot*0x100)
|
c.rom.base = uint16(0xc000 + slot*0x100)
|
||||||
a.mmu.setPagesRead(uint8(0xc0+slot), uint8(0xc0+slot), c.rom)
|
a.mmu.setCardROM(slot, c.rom)
|
||||||
}
|
}
|
||||||
|
|
||||||
if slot != 0 && c.romExtra != nil {
|
if slot != 0 && c.romExtra != nil {
|
||||||
c.romExtra.base = uint16(0xc800)
|
c.romExtra.base = uint16(0xc800)
|
||||||
a.mmu.prepareCardExtraRom(slot, c.romExtra)
|
a.mmu.setCardROMExtra(slot, c.romExtra)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 0x10; i++ {
|
for i := 0; i < 0x10; i++ {
|
||||||
|
|
|
@ -32,14 +32,11 @@ type cardLanguage struct {
|
||||||
cardBase
|
cardBase
|
||||||
readState bool
|
readState bool
|
||||||
writeState uint8
|
writeState uint8
|
||||||
activeBank uint8
|
altBank bool
|
||||||
ramBankA *memoryRange // First 4kb to map in 0xD000-0xDFFF
|
|
||||||
ramBankB *memoryRange // Second 4kb to map in 0xD000-0xDFFF
|
|
||||||
ramUpper *memoryRange // Upper 8kb to map in 0xE000-0xFFFF
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Write enabling requires two sofstwitch accesses
|
// Write enabling requires two softswitch accesses
|
||||||
lcWriteDisabled = 0
|
lcWriteDisabled = 0
|
||||||
lcWriteHalfEnabled = 1
|
lcWriteHalfEnabled = 1
|
||||||
lcWriteEnabled = 2
|
lcWriteEnabled = 2
|
||||||
|
@ -48,11 +45,9 @@ const (
|
||||||
func (c *cardLanguage) assign(a *Apple2, slot int) {
|
func (c *cardLanguage) assign(a *Apple2, slot int) {
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState = lcWriteEnabled
|
c.writeState = lcWriteEnabled
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
|
|
||||||
c.ramBankA = newMemoryRange(0xd000, make([]uint8, 0x1000))
|
a.mmu.initLanguageRAM(1)
|
||||||
c.ramBankB = newMemoryRange(0xd000, make([]uint8, 0x1000))
|
|
||||||
c.ramUpper = newMemoryRange(0xe000, make([]uint8, 0x2000))
|
|
||||||
|
|
||||||
for i := uint8(0x0); i <= 0xf; i++ {
|
for i := uint8(0x0); i <= 0xf; i++ {
|
||||||
iCopy := i
|
iCopy := i
|
||||||
|
@ -70,7 +65,7 @@ func (c *cardLanguage) assign(a *Apple2, slot int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardLanguage) ssAction(ss uint8, write bool) {
|
func (c *cardLanguage) ssAction(ss uint8, write bool) {
|
||||||
c.activeBank = (ss >> 3) & 1
|
c.altBank = ((ss >> 3) & 1) == 1
|
||||||
action := ss & 0x3
|
action := ss & 0x3
|
||||||
switch action {
|
switch action {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -103,30 +98,8 @@ func (c *cardLanguage) ssAction(ss uint8, write bool) {
|
||||||
c.applyState()
|
c.applyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardLanguage) getActiveBank() *memoryRange {
|
|
||||||
if c.activeBank == 0 {
|
|
||||||
return c.ramBankA
|
|
||||||
}
|
|
||||||
return c.ramBankB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardLanguage) applyState() {
|
func (c *cardLanguage) applyState() {
|
||||||
mmu := c.a.mmu
|
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
|
||||||
|
|
||||||
if c.readState {
|
|
||||||
mmu.setPagesRead(0xd0, 0xdf, c.getActiveBank())
|
|
||||||
mmu.setPagesRead(0xe0, 0xff, c.ramUpper)
|
|
||||||
} else {
|
|
||||||
mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.writeState == lcWriteEnabled {
|
|
||||||
mmu.setPagesWrite(0xd0, 0xdf, c.getActiveBank())
|
|
||||||
mmu.setPagesWrite(0xe0, 0xff, c.ramUpper)
|
|
||||||
} else {
|
|
||||||
mmu.setPagesWrite(0xd0, 0xff, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardLanguage) save(w io.Writer) error {
|
func (c *cardLanguage) save(w io.Writer) error {
|
||||||
|
@ -138,23 +111,10 @@ func (c *cardLanguage) save(w io.Writer) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = binary.Write(w, binary.BigEndian, c.activeBank)
|
err = binary.Write(w, binary.BigEndian, c.altBank)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = c.ramBankA.save(w)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = c.ramBankB.save(w)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = c.ramUpper.save(w)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.cardBase.save(w)
|
return c.cardBase.save(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,23 +127,10 @@ func (c *cardLanguage) load(r io.Reader) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = binary.Read(r, binary.BigEndian, &c.activeBank)
|
err = binary.Read(r, binary.BigEndian, &c.altBank)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = c.ramBankA.load(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = c.ramBankB.load(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = c.ramUpper.load(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.applyState()
|
c.applyState()
|
||||||
return c.cardBase.load(r)
|
return c.cardBase.load(r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,20 +24,5 @@ func (c *cardLogger) assign(a *Apple2, slot int) {
|
||||||
}, "LOGGERW")
|
}, "LOGGERW")
|
||||||
}
|
}
|
||||||
|
|
||||||
if slot != 0 {
|
|
||||||
a.mmu.setPagesRead(uint8(0xc0+slot), uint8(0xc0+slot), c)
|
|
||||||
}
|
|
||||||
c.cardBase.assign(a, slot)
|
c.cardBase.assign(a, slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryHandler implementation
|
|
||||||
func (c *cardLogger) peek(address uint16) uint8 {
|
|
||||||
fmt.Printf("[cardLogger] Read in %x.\n", address)
|
|
||||||
c.a.dumpDebugInfo()
|
|
||||||
|
|
||||||
return 0xf3
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*cardLogger) poke(address uint16, value uint8) {
|
|
||||||
fmt.Printf("[cardLogger] Write %x in %x.\n", value, address)
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,20 +17,10 @@ type cardSaturn struct {
|
||||||
cardBase
|
cardBase
|
||||||
readState bool
|
readState bool
|
||||||
writeState uint8
|
writeState uint8
|
||||||
activeBank uint8
|
altBank bool
|
||||||
activeBlock uint8
|
activeBlock uint8
|
||||||
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 (
|
|
||||||
// Write enabling requires two sofstwitch accesses
|
|
||||||
saturnWriteDisabled = 0
|
|
||||||
saturnWriteHalfEnabled = 1
|
|
||||||
saturnWriteEnabled = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
saturnBlocks = 8
|
saturnBlocks = 8
|
||||||
)
|
)
|
||||||
|
@ -38,13 +28,10 @@ const (
|
||||||
func (c *cardSaturn) assign(a *Apple2, slot int) {
|
func (c *cardSaturn) assign(a *Apple2, slot int) {
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState = lcWriteEnabled
|
c.writeState = lcWriteEnabled
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
|
c.activeBlock = 0
|
||||||
|
a.mmu.initLanguageRAM(saturnBlocks)
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
for i := uint8(0x0); i <= 0xf; i++ {
|
for i := uint8(0x0); i <= 0xf; i++ {
|
||||||
iCopy := i
|
iCopy := i
|
||||||
c.addCardSoftSwitchR(iCopy, func(*ioC0Page) uint8 {
|
c.addCardSoftSwitchR(iCopy, func(*ioC0Page) uint8 {
|
||||||
|
@ -63,22 +50,22 @@ func (c *cardSaturn) ssAction(ss uint8, write bool) {
|
||||||
switch ss {
|
switch ss {
|
||||||
case 0:
|
case 0:
|
||||||
// RAM read, no writes
|
// RAM read, no writes
|
||||||
c.activeBank = 0
|
c.altBank = false
|
||||||
c.readState = true
|
c.readState = true
|
||||||
c.writeState = lcWriteDisabled
|
c.writeState = lcWriteDisabled
|
||||||
case 1:
|
case 1:
|
||||||
// ROM read, RAM write
|
// ROM read, RAM write
|
||||||
c.activeBank = 0
|
c.altBank = false
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState++
|
c.writeState++
|
||||||
case 2:
|
case 2:
|
||||||
// ROM read, no writes
|
// ROM read, no writes
|
||||||
c.activeBank = 0
|
c.altBank = false
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState = lcWriteDisabled
|
c.writeState = lcWriteDisabled
|
||||||
case 3:
|
case 3:
|
||||||
//RAM read, RAM write
|
//RAM read, RAM write
|
||||||
c.activeBank = 0
|
c.altBank = false
|
||||||
c.readState = true
|
c.readState = true
|
||||||
c.writeState++
|
c.writeState++
|
||||||
case 4:
|
case 4:
|
||||||
|
@ -91,22 +78,22 @@ func (c *cardSaturn) ssAction(ss uint8, write bool) {
|
||||||
c.activeBlock = 3
|
c.activeBlock = 3
|
||||||
case 8:
|
case 8:
|
||||||
// RAM read, no writes
|
// RAM read, no writes
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
c.readState = true
|
c.readState = true
|
||||||
c.writeState = lcWriteDisabled
|
c.writeState = lcWriteDisabled
|
||||||
case 9:
|
case 9:
|
||||||
// ROM read, RAM write
|
// ROM read, RAM write
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState++
|
c.writeState++
|
||||||
case 10:
|
case 10:
|
||||||
// ROM read, no writes
|
// ROM read, no writes
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState = lcWriteDisabled
|
c.writeState = lcWriteDisabled
|
||||||
case 11:
|
case 11:
|
||||||
//RAM read, RAM write
|
//RAM read, RAM write
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
c.readState = true
|
c.readState = true
|
||||||
c.writeState++
|
c.writeState++
|
||||||
case 12:
|
case 12:
|
||||||
|
@ -131,30 +118,9 @@ func (c *cardSaturn) ssAction(ss uint8, write bool) {
|
||||||
c.applyState()
|
c.applyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardSaturn) getActiveBank() [8]*memoryRange {
|
|
||||||
if c.activeBank == 0 {
|
|
||||||
return c.ramBankA
|
|
||||||
}
|
|
||||||
return c.ramBankB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardSaturn) applyState() {
|
func (c *cardSaturn) applyState() {
|
||||||
mmu := c.a.mmu
|
c.a.mmu.setLanguageRAMBlock(c.activeBlock)
|
||||||
block := c.activeBlock
|
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
|
||||||
|
|
||||||
if c.readState {
|
|
||||||
mmu.setPagesRead(0xd0, 0xdf, c.getActiveBank()[block])
|
|
||||||
mmu.setPagesRead(0xe0, 0xff, c.ramUpper[block])
|
|
||||||
} else {
|
|
||||||
mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.writeState == lcWriteEnabled {
|
|
||||||
mmu.setPagesWrite(0xd0, 0xdf, c.getActiveBank()[block])
|
|
||||||
mmu.setPagesWrite(0xe0, 0xff, c.ramUpper[block])
|
|
||||||
} else {
|
|
||||||
mmu.setPagesWrite(0xd0, 0xff, nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardSaturn) save(w io.Writer) error {
|
func (c *cardSaturn) save(w io.Writer) error {
|
||||||
|
@ -167,7 +133,7 @@ func (c *cardSaturn) save(w io.Writer) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = binary.Write(w, binary.BigEndian, c.activeBank)
|
err = binary.Write(w, binary.BigEndian, c.altBank)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -175,18 +141,6 @@ func (c *cardSaturn) save(w io.Writer) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = c.ramBankA[i].save(w)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = c.ramBankB[i].save(w)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = c.ramUpper[i].save(w)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return c.cardBase.save(w)
|
return c.cardBase.save(w)
|
||||||
}
|
}
|
||||||
|
@ -201,7 +155,7 @@ func (c *cardSaturn) load(r io.Reader) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = binary.Read(r, binary.BigEndian, &c.activeBank)
|
err = binary.Read(r, binary.BigEndian, &c.altBank)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -209,16 +163,6 @@ func (c *cardSaturn) load(r io.Reader) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = c.ramBankA[i].load(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = c.ramBankB[i].load(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = c.ramUpper[i].load(r)
|
|
||||||
|
|
||||||
c.applyState()
|
c.applyState()
|
||||||
}
|
}
|
||||||
return c.cardBase.load(r)
|
return c.cardBase.load(r)
|
||||||
|
|
|
@ -114,6 +114,9 @@ func (p *ioC0Page) peek(address uint16) uint8 {
|
||||||
pageAddress := uint8(address)
|
pageAddress := uint8(address)
|
||||||
ss := p.softSwitchesR[pageAddress]
|
ss := p.softSwitchesR[pageAddress]
|
||||||
if ss == nil {
|
if ss == nil {
|
||||||
|
if p.trace {
|
||||||
|
fmt.Printf("Unknown softswitch on read to $%04x\n", address)
|
||||||
|
}
|
||||||
if p.panicNotImplemented {
|
if p.panicNotImplemented {
|
||||||
panic(fmt.Sprintf("Unknown softswitch on read to $%04x", address))
|
panic(fmt.Sprintf("Unknown softswitch on read to $%04x", address))
|
||||||
}
|
}
|
||||||
|
@ -131,6 +134,9 @@ func (p *ioC0Page) poke(address uint16, value uint8) {
|
||||||
pageAddress := uint8(address)
|
pageAddress := uint8(address)
|
||||||
ss := p.softSwitchesW[pageAddress]
|
ss := p.softSwitchesW[pageAddress]
|
||||||
if ss == nil {
|
if ss == nil {
|
||||||
|
if p.trace {
|
||||||
|
fmt.Printf("Unknown softswitch on write to $%04x\n", address)
|
||||||
|
}
|
||||||
if p.panicNotImplemented {
|
if p.panicNotImplemented {
|
||||||
panic(fmt.Sprintf("Unknown softswitch on write to $%04x", address))
|
panic(fmt.Sprintf("Unknown softswitch on write to $%04x", address))
|
||||||
}
|
}
|
||||||
|
|
309
memoryManager.go
309
memoryManager.go
|
@ -2,6 +2,7 @@ package apple2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,137 +11,218 @@ import (
|
||||||
|
|
||||||
type memoryManager struct {
|
type memoryManager struct {
|
||||||
apple2 *Apple2
|
apple2 *Apple2
|
||||||
// Map of assigned pages
|
|
||||||
activeMemoryRead [256]memoryHandler
|
|
||||||
activeMemoryWrite [256]memoryHandler
|
|
||||||
|
|
||||||
// Pages prepared to be paged in and out
|
// Main RAM area: 0x0000 to 0xbfff
|
||||||
physicalMainRAM *memoryRange // 0x0000 to 0xbfff, Up to 48 Kb
|
physicalMainRAM *memoryRange // 0x0000 to 0xbfff, Up to 48 Kb
|
||||||
physicalROM memoryHandler // 0xd000 to 0xffff, 12 Kb
|
physicalMainRAMAlt *memoryRange // 0x0000 to 0xbfff, Up to 48 Kb. Additional
|
||||||
physicalROMe memoryHandler // 0xc000 to 0xcfff, Zero or 4bk in the Apple2e
|
|
||||||
|
|
||||||
// Pages prapared for optional card ROM banks
|
// Slots area: 0xc000 to 0xcfff
|
||||||
activeSlot uint8
|
cardsROM [8]memoryHandler //0xcs00 to 0xcsff. 256 bytes for each card
|
||||||
cardsROMExtra [8]memoryHandler // 0xc800 to 0xcfff. for each card
|
cardsROMExtra [8]memoryHandler // 0xc800 to 0xcfff. 2048 bytes for each card
|
||||||
|
physicalROMe memoryHandler // 0xc100 to 0xcfff, Zero or 4kb in the Apple2e
|
||||||
|
|
||||||
|
// Upper area: 0xd000 to 0xffff
|
||||||
|
physicalROM [4]memoryHandler // 0xd000 to 0xffff, 12 Kb. Up to four banks
|
||||||
|
physicalDRAM []memoryHandler // 0xd000 to 0xdfff, 4KB. Up to 8 banks.
|
||||||
|
physicalDAltRAM []memoryHandler // 0xd000 to 0xdfff, 4KB. Up to 8 banks.
|
||||||
|
physicalEFRAM []memoryHandler // 0xe000 to 0xffff, 8KB. Up to 8 banks.
|
||||||
|
|
||||||
|
// Pages prapared for optional card ROM banks in 0xc800 to 0xcfff
|
||||||
|
altMainRAMActiveRead bool // Use extra RAM on the 128KB Apple2e for read
|
||||||
|
altMainRAMActiveWrite bool // Use extra RAM on the 128KB Apple2e for write
|
||||||
|
|
||||||
|
c3ROMActive bool // Apple2e slot 3 ROM shadow
|
||||||
|
cxROMActive bool // Apple2e slots ROM shadow
|
||||||
|
activeSlot uint8 // Active slot owner of 0xc800 to 0xcfff
|
||||||
|
|
||||||
|
lcSelectedBlock uint8 // Language card block selected. Usually, allways 0. But Saturn has 8
|
||||||
|
lcActiveRead bool // Upper RAM active for read
|
||||||
|
lcActiveWrite bool // Upper RAM active for read
|
||||||
|
lcAltBank bool // Alternate
|
||||||
|
romPage uint8 // Active ROM page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ioC8Off uint16 = 0xcfff
|
||||||
|
addressLimitMainRAM uint16 = 0xbfff
|
||||||
|
addressLimitIO uint16 = 0xc0ff
|
||||||
|
addressLimitSlots uint16 = 0xc7ff
|
||||||
|
addressLimitSlotsExtra uint16 = 0xcfff
|
||||||
|
addressLimitDArea uint16 = 0xdfff
|
||||||
|
)
|
||||||
|
|
||||||
type memoryHandler interface {
|
type memoryHandler interface {
|
||||||
peek(uint16) uint8
|
peek(uint16) uint8
|
||||||
poke(uint16, uint8)
|
poke(uint16, uint8)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
ioC8Off uint16 = 0xCFFF
|
|
||||||
)
|
|
||||||
|
|
||||||
func (mmu *memoryManager) access(address uint16, activeMemory *[256]memoryHandler) memoryHandler {
|
|
||||||
if address == ioC8Off {
|
|
||||||
mmu.resetSlotExpansionRoms()
|
|
||||||
}
|
|
||||||
|
|
||||||
hi := uint8(address >> 8)
|
|
||||||
if hi >= 0xC1 && hi <= 0xC7 {
|
|
||||||
slot := hi - 0xC0
|
|
||||||
if slot != mmu.activeSlot {
|
|
||||||
mmu.activateCardRomExtra(slot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mh := activeMemory[hi]
|
|
||||||
if mh == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return mh
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek returns the data on the given address
|
|
||||||
func (mmu *memoryManager) Peek(address uint16) uint8 {
|
|
||||||
mh := mmu.access(address, &mmu.activeMemoryRead)
|
|
||||||
if mh == nil {
|
|
||||||
return 0xf4 // Or some random number
|
|
||||||
}
|
|
||||||
return mh.peek(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poke sets the data at the given address
|
|
||||||
func (mmu *memoryManager) Poke(address uint16, value uint8) {
|
|
||||||
mh := mmu.access(address, &mmu.activeMemoryWrite)
|
|
||||||
if mh == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mh.poke(address, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mmu *memoryManager) setPages(begin uint8, end uint8, mh memoryHandler) {
|
|
||||||
mmu.setPagesRead(begin, end, mh)
|
|
||||||
mmu.setPagesWrite(begin, end, mh)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mmu *memoryManager) setPagesRead(begin uint8, end uint8, mh memoryHandler) {
|
|
||||||
i := begin
|
|
||||||
for {
|
|
||||||
mmu.activeMemoryRead[i] = mh
|
|
||||||
if i == end {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mmu *memoryManager) setPagesWrite(begin uint8, end uint8, mh memoryHandler) {
|
|
||||||
i := begin
|
|
||||||
for {
|
|
||||||
mmu.activeMemoryWrite[i] = mh
|
|
||||||
if i == end {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mmu *memoryManager) prepareCardExtraRom(slot int, mh memoryHandler) {
|
|
||||||
mmu.cardsROMExtra[slot] = mh
|
|
||||||
}
|
|
||||||
|
|
||||||
// When 0xcfff is accessed the card expansion rom is unassigned
|
|
||||||
func (mmu *memoryManager) resetSlotExpansionRoms() {
|
|
||||||
if mmu.apple2.io.isSoftSwitchActive(ioFlagIntCxRom) {
|
|
||||||
// Ignore if the Apple2 shadow ROM is active
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mmu.activeSlot = 0
|
|
||||||
mmu.setPagesRead(0xc8, 0xcf, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When a card base ROM is accesed the extra rom is assigned if available
|
|
||||||
func (mmu *memoryManager) activateCardRomExtra(slot uint8) {
|
|
||||||
//fmt.Printf("Activate slot %v\n", slot)
|
|
||||||
if mmu.cardsROMExtra[slot] != nil {
|
|
||||||
mmu.setPagesRead(0xC8, 0xCF, mmu.cardsROMExtra[slot])
|
|
||||||
}
|
|
||||||
mmu.activeSlot = slot
|
|
||||||
}
|
|
||||||
|
|
||||||
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) *memoryManager {
|
func newMemoryManager(a *Apple2) *memoryManager {
|
||||||
var mmu memoryManager
|
var mmu memoryManager
|
||||||
mmu.apple2 = a
|
mmu.apple2 = a
|
||||||
|
|
||||||
ram := make([]uint8, 0xc000) // Reserve 48kb
|
ram := make([]uint8, 0xc000) // Reserve 48kb
|
||||||
mmu.physicalMainRAM = newMemoryRange(0, ram)
|
mmu.physicalMainRAM = newMemoryRange(0, ram)
|
||||||
mmu.resetBaseRamPaging()
|
|
||||||
|
|
||||||
return &mmu
|
return &mmu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mmu *memoryManager) accessRead(address uint16) memoryHandler {
|
||||||
|
// Main RAM area
|
||||||
|
if address <= addressLimitMainRAM {
|
||||||
|
if mmu.altMainRAMActiveRead {
|
||||||
|
return mmu.physicalMainRAMAlt
|
||||||
|
}
|
||||||
|
return mmu.physicalMainRAM
|
||||||
|
}
|
||||||
|
|
||||||
|
// IO section
|
||||||
|
if address <= addressLimitIO {
|
||||||
|
return mmu.apple2.io
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slots sections
|
||||||
|
if address <= addressLimitSlotsExtra {
|
||||||
|
slot := uint8((address >> 8) & 0x07)
|
||||||
|
if mmu.cxROMActive {
|
||||||
|
return mmu.physicalROMe
|
||||||
|
}
|
||||||
|
// First slot area
|
||||||
|
if address <= addressLimitSlots {
|
||||||
|
if mmu.c3ROMActive && (slot == 3) {
|
||||||
|
return mmu.physicalROMe
|
||||||
|
}
|
||||||
|
mmu.activeSlot = slot
|
||||||
|
return mmu.cardsROM[slot]
|
||||||
|
}
|
||||||
|
// Extra slot area
|
||||||
|
if address == ioC8Off {
|
||||||
|
// Reset extra slot area owner
|
||||||
|
mmu.activeSlot = 0
|
||||||
|
}
|
||||||
|
return mmu.cardsROMExtra[slot]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upper address area
|
||||||
|
if mmu.lcActiveRead {
|
||||||
|
if address <= addressLimitDArea {
|
||||||
|
if mmu.lcAltBank {
|
||||||
|
return mmu.physicalDAltRAM[mmu.lcSelectedBlock]
|
||||||
|
}
|
||||||
|
return mmu.physicalDRAM[mmu.lcSelectedBlock]
|
||||||
|
}
|
||||||
|
return mmu.physicalEFRAM[mmu.lcSelectedBlock]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ROM
|
||||||
|
return mmu.physicalROM[mmu.romPage]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mmu *memoryManager) accessWrite(address uint16) memoryHandler {
|
||||||
|
// Main RAM area
|
||||||
|
if address <= addressLimitMainRAM {
|
||||||
|
if mmu.altMainRAMActiveWrite {
|
||||||
|
return mmu.physicalMainRAMAlt
|
||||||
|
}
|
||||||
|
return mmu.physicalMainRAM
|
||||||
|
}
|
||||||
|
|
||||||
|
// IO section
|
||||||
|
if address <= addressLimitIO {
|
||||||
|
return mmu.apple2.io
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slots sections
|
||||||
|
if address <= addressLimitSlotsExtra {
|
||||||
|
slot := uint8((address >> 8) & 0x07)
|
||||||
|
// First slot area
|
||||||
|
if address <= addressLimitSlots {
|
||||||
|
mmu.activeSlot = slot
|
||||||
|
return mmu.cardsROM[slot]
|
||||||
|
}
|
||||||
|
// Extra slot area
|
||||||
|
if address == ioC8Off {
|
||||||
|
// Reset extra slot area owner
|
||||||
|
mmu.activeSlot = 0
|
||||||
|
}
|
||||||
|
return mmu.cardsROMExtra[slot]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upper address area
|
||||||
|
if mmu.lcActiveWrite {
|
||||||
|
if address <= addressLimitDArea {
|
||||||
|
if mmu.lcAltBank {
|
||||||
|
return mmu.physicalDAltRAM[mmu.lcSelectedBlock]
|
||||||
|
}
|
||||||
|
return mmu.physicalDRAM[mmu.lcSelectedBlock]
|
||||||
|
}
|
||||||
|
return mmu.physicalEFRAM[mmu.lcSelectedBlock]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ROM
|
||||||
|
return mmu.physicalROM[mmu.romPage]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek returns the data on the given address
|
||||||
|
func (mmu *memoryManager) Peek(address uint16) uint8 {
|
||||||
|
mh := mmu.accessRead(address)
|
||||||
|
if mh == nil {
|
||||||
|
fmt.Printf("Reading void addressing 0x%x\n", address)
|
||||||
|
return 0xf4 // Or some random number
|
||||||
|
}
|
||||||
|
return mh.peek(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poke sets the data at the given address
|
||||||
|
func (mmu *memoryManager) Poke(address uint16, value uint8) {
|
||||||
|
mh := mmu.accessWrite(address)
|
||||||
|
if mh == nil {
|
||||||
|
fmt.Printf("Writing to void addressing 0x%x\n", address)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mh.poke(address, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory initialization
|
||||||
|
func (mmu *memoryManager) setCardROM(slot int, mh memoryHandler) {
|
||||||
|
mmu.cardsROM[slot] = mh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mmu *memoryManager) setCardROMExtra(slot int, mh memoryHandler) {
|
||||||
|
mmu.cardsROMExtra[slot] = mh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mmu *memoryManager) initLanguageRAM(groups int) {
|
||||||
|
mmu.physicalDRAM = make([]memoryHandler, groups)
|
||||||
|
mmu.physicalDAltRAM = make([]memoryHandler, groups)
|
||||||
|
mmu.physicalEFRAM = make([]memoryHandler, groups)
|
||||||
|
for i := 0; i < groups; i++ {
|
||||||
|
mmu.physicalDRAM[i] = newMemoryRange(0xd000, make([]uint8, 0x1000))
|
||||||
|
mmu.physicalDAltRAM[i] = newMemoryRange(0xd000, make([]uint8, 0x1000))
|
||||||
|
mmu.physicalEFRAM[i] = newMemoryRange(0xe000, make([]uint8, 0x2000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory configuration
|
||||||
|
func (mmu *memoryManager) setActiveROMPage(page uint8) {
|
||||||
|
mmu.romPage = page
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mmu *memoryManager) getActiveROMPage() uint8 {
|
||||||
|
return mmu.romPage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mmu *memoryManager) setLanguageRAM(readActive bool, writeActive bool, altBank bool) {
|
||||||
|
mmu.lcActiveRead = readActive
|
||||||
|
mmu.lcActiveWrite = writeActive
|
||||||
|
mmu.lcAltBank = altBank
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mmu *memoryManager) setLanguageRAMBlock(block uint8) {
|
||||||
|
block = block % uint8(len(mmu.physicalDRAM))
|
||||||
|
mmu.lcSelectedBlock = block
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: complete save and load
|
||||||
func (mmu *memoryManager) save(w io.Writer) error {
|
func (mmu *memoryManager) save(w io.Writer) error {
|
||||||
err := mmu.physicalMainRAM.save(w)
|
err := mmu.physicalMainRAM.save(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -162,8 +244,7 @@ func (mmu *memoryManager) load(r io.Reader) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mmu.activateCardRomExtra(mmu.activeSlot)
|
// mmu.activateCardRomExtra(mmu.activeSlot)
|
||||||
|
|
||||||
mmu.resetBaseRamPaging()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,21 +82,21 @@ func getSoftSwitchExt(ioFlag uint8, dstValue uint8, action softSwitchExtAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
func softSwitchIntCxRomOn(io *ioC0Page) {
|
func softSwitchIntCxRomOn(io *ioC0Page) {
|
||||||
io.apple2.mmu.setPagesRead(0xc1, 0xcf, io.apple2.mmu.physicalROMe)
|
//io.apple2.mmu.setPagesRead(0xc1, 0xcf, io.apple2.mmu.physicalROMe)
|
||||||
}
|
}
|
||||||
|
|
||||||
func softSwitchIntCxRomOff(io *ioC0Page) {
|
func softSwitchIntCxRomOff(io *ioC0Page) {
|
||||||
// TODO restore all the ROM from the slots for 0xc1 to 0xc7
|
// TODO restore all the ROM from the slots for 0xc1 to 0xc7
|
||||||
io.apple2.mmu.setPages(0xc1, 0xc7, nil)
|
//io.apple2.mmu.setPages(0xc1, 0xc7, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func softSwitchSlotC3RomOn(io *ioC0Page) {
|
func softSwitchSlotC3RomOn(io *ioC0Page) {
|
||||||
// TODO restore the slot 3 ROM
|
// TODO restore the slot 3 ROM
|
||||||
io.apple2.mmu.setPages(0xc3, 0xc3, nil)
|
//io.apple2.mmu.setPages(0xc3, 0xc3, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func softSwitchSlotC3RomOff(io *ioC0Page) {
|
func softSwitchSlotC3RomOff(io *ioC0Page) {
|
||||||
io.apple2.mmu.setPagesRead(0xc3, 0xc3, io.apple2.mmu.physicalROMe)
|
//io.apple2.mmu.setPagesRead(0xc3, 0xc3, io.apple2.mmu.physicalROMe)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: apply state after persistance load
|
// TODO: apply state after persistance load
|
||||||
|
|
Loading…
Reference in New Issue