mirror of
https://github.com/ivanizag/izapple2.git
synced 2026-10-24 12:06:34 +00:00
The emulation writes to a disk in the file it was loaded from, so saving a game modifies the image the user has. With a save directory the changes go to an overlay file there instead and the image is only ever read. An overlay stores just the parts the software wrote, indexed by a bitmap in its header: tracks of 4096 bytes for the diskettes, blocks of 512 for the ProDOS devices. Nothing is written for the rest, so the file takes as much space as what was saved and not the size of the disk, which matters for the hard disks where an image can be 32Mb and a saved game a handful of blocks. The header also holds a checksum of the image it was made from, so an overlay is never applied to a different disk with the same name. As the image is only read, this also makes writable the disks that could not be written at all: the ones inside a zip or a gzip, the ones loaded from an URL and the ones embedded in the executable. The directory comes from the new -saveDir option. The cards that write to their disks take a savedir parameter of their own that defaults to it, with 'none' to write back to the image. Also fixes saveTrack leaking a file descriptor on every track written, and going on to write through a nil file when it could not be opened.
278 lines
6.6 KiB
Go
278 lines
6.6 KiB
Go
package izapple2
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ivanizag/iz6502"
|
|
)
|
|
|
|
func configure(configuration *configuration) (*Apple2, error) {
|
|
|
|
if configuration.getFlag(confShowConfig) {
|
|
configuration.dump()
|
|
os.Exit(0)
|
|
}
|
|
|
|
var a Apple2
|
|
a.Name = configuration.get(confName)
|
|
a.mmu = newMemoryManager(&a)
|
|
a.video = newVideo(&a)
|
|
a.io = newIoC0Page(&a)
|
|
a.commandChannel = make(chan command, 100)
|
|
|
|
// Configure the board
|
|
board := configuration.get(confBoard)
|
|
a.board = board
|
|
|
|
err := setupCharactedGenerator(&a, board, configuration.get(confCharRom))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addApple2SoftSwitches(a.io)
|
|
switch board {
|
|
case "2plus":
|
|
a.mmu.initMainRAM()
|
|
case "2e":
|
|
a.isApple2e = true
|
|
a.mmu.initMainRAM()
|
|
a.mmu.initExtendedRAM(1)
|
|
a.hasLowerCase = true
|
|
addApple2ESoftSwitches(a.io)
|
|
case "base64a":
|
|
a.mmu.initMainRAM()
|
|
a.hasLowerCase = true
|
|
addBase64aSoftSwitches(a.io)
|
|
case "basis108":
|
|
memBasis108 := newMemoryRangeBasis108()
|
|
videoBasis108 := newVideoBasis108(&a, memBasis108)
|
|
a.mmu.initCustomRAM(memBasis108)
|
|
a.video = videoBasis108
|
|
a.hasLowerCase = true
|
|
addBasis108SoftSwitches(a.io, memBasis108, videoBasis108, a.cg)
|
|
default:
|
|
return nil, fmt.Errorf("board %s not supported it must be '2plus', '2e', 'base64a', 'basis108", board)
|
|
}
|
|
|
|
cpu := configuration.get(confCpu)
|
|
switch cpu {
|
|
case "6502":
|
|
a.cpu = iz6502.NewNMOS6502(a.mmu)
|
|
case "65c02":
|
|
a.cpu = iz6502.NewCMOS65c02(a.mmu)
|
|
default:
|
|
return nil, fmt.Errorf("cpu %s not supported, must be '6502' or '65c02'", cpu)
|
|
}
|
|
|
|
err = a.loadRom(configuration.get(confRom))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.setProfiling(configuration.getFlag(confProfile))
|
|
a.SetForceCaps(configuration.getFlag(confForceCaps))
|
|
|
|
err = a.setClockSpeed(configuration.get(confSpeed))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Where the cards that write to their disks keep the changes, unless they
|
|
// name a directory of their own
|
|
saveDir := configuration.get(confSaveDir)
|
|
if saveDir == "none" {
|
|
saveDir = ""
|
|
}
|
|
|
|
// Add cards on the slots
|
|
for i := range 8 {
|
|
cardConfig := configuration.get(fmt.Sprintf("s%v", i))
|
|
if cardConfig != "" {
|
|
_, err := setupCard(&a, i, cardConfig, saveDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add mods
|
|
mods := strings.SplitSeq(configuration.get(confMods), ",")
|
|
for mod := range mods {
|
|
switch strings.TrimSpace(mod) {
|
|
// case "shift":
|
|
// setupShiftedKeyboard(a)
|
|
case "four-colors":
|
|
// This removes the mod to have 6 colors sent by Wozniak to Byte
|
|
// magazine. See: https://archive.org/details/byte-magazine-1979-06/page/n67/mode/2up?view=theater
|
|
a.isFourColors = true
|
|
}
|
|
}
|
|
|
|
// Add optional accesories including the aux slot
|
|
ramWorksSize := configuration.get(confRamworks)
|
|
if ramWorksSize != "" && ramWorksSize != "none" {
|
|
err = setupRAMWorksCard(&a, ramWorksSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if configuration.getFlag(confRgb) {
|
|
setupRGBCard(&a)
|
|
}
|
|
|
|
tape := configuration.get(confTape)
|
|
if tape != "" && tape != "none" {
|
|
data, _, err := LoadResource(tape)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c, err := newCassette(&a, data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not load the tape %s: %w", tape, err)
|
|
}
|
|
a.io.setCassette(c)
|
|
}
|
|
|
|
nsc := configuration.get(confNsc)
|
|
if nsc != "none" && nsc != "" {
|
|
err = setupNoSlotClock(&a, nsc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if configuration.getFlag(confRomx) {
|
|
err := setupRomX(&a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = setupTracers(&a, configuration.get(confTrace))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &a, nil
|
|
}
|
|
|
|
func (a *Apple2) setClockSpeed(speed string) error {
|
|
if speed == "full" {
|
|
a.cycleDurationNs = 0
|
|
} else if speed == "ntsc" {
|
|
a.cycleDurationNs = 1000.0 / CPUClockMhz
|
|
} else if speed == "pal" {
|
|
a.cycleDurationNs = 1000.0 / cpuClockEuroMhz
|
|
} else {
|
|
clockMhz, err := strconv.ParseFloat(speed, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid clock speed: %s", speed)
|
|
}
|
|
a.cycleDurationNs = 1000.0 / clockMhz
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Apple2) setProfiling(value bool) {
|
|
a.profile = value
|
|
}
|
|
|
|
// SetForceCaps allows the caps state to be toggled at runtime
|
|
func (a *Apple2) SetForceCaps(value bool) {
|
|
a.forceCaps = value
|
|
}
|
|
|
|
func (a *Apple2) loadRom(filename string) error {
|
|
if filename == "<custom>" {
|
|
switch a.board {
|
|
case "base64a":
|
|
return loadBase64aRom(a)
|
|
case "basis108":
|
|
return loadBasis108Rom(a)
|
|
default:
|
|
return fmt.Errorf("no custom ROM defined for board %s", a.board)
|
|
}
|
|
}
|
|
|
|
data, _, err := LoadResource(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
size := len(data)
|
|
|
|
romBase := 0x10000 - size
|
|
a.mmu.physicalROM = newMemoryRangeROM(uint16(romBase), data, "Main ROM")
|
|
return nil
|
|
}
|
|
|
|
const pagedRomChipWindowSize = 0x800 // 2 KB
|
|
const pagedRomChipCount = 6 // There has to be six ROM chips
|
|
const pagedRomWindowSize = pagedRomChipWindowSize * pagedRomChipCount // To cover 0xd000 to 0xffff
|
|
func loadMultiPageRom(a *Apple2, filenames []string) error {
|
|
if len(filenames) != pagedRomChipCount {
|
|
return fmt.Errorf("expected %d ROM files, got %d", pagedRomChipCount, len(filenames))
|
|
}
|
|
|
|
// Load the 6 PROM dumps
|
|
proms := make([][]uint8, pagedRomChipCount)
|
|
banks := 1
|
|
for i, filename := range filenames {
|
|
var err error
|
|
proms[i], _, err = LoadResource(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pages := len(proms[i]) / pagedRomChipWindowSize
|
|
if pages > banks {
|
|
banks = pages
|
|
}
|
|
}
|
|
|
|
// Init the array of banks
|
|
romBanksBytes := make([][]uint8, banks)
|
|
for bank := range romBanksBytes {
|
|
romBanksBytes[bank] = make([]uint8, 0, pagedRomWindowSize)
|
|
}
|
|
|
|
// Distribute the per chip banks on the full rom banks
|
|
for _, romData := range proms {
|
|
for bank := range romBanksBytes {
|
|
start := (bank * pagedRomChipWindowSize) % len(romData)
|
|
romBanksBytes[bank] = append(romBanksBytes[bank], romData[start:start+pagedRomChipWindowSize]...)
|
|
}
|
|
}
|
|
|
|
// Create paged ROM
|
|
romData := make([]uint8, 0, pagedRomWindowSize*banks)
|
|
for _, bank := range romBanksBytes {
|
|
romData = append(romData, bank...)
|
|
}
|
|
rom := newMemoryRangePagedROM(0xd000, romData, "Multipage main ROM", uint8(banks))
|
|
|
|
// Start with first bank active
|
|
rom.setPage(0)
|
|
|
|
a.mmu.physicalROM = rom
|
|
return nil
|
|
}
|
|
|
|
// CreateConfiguredApple is a device independent main. Video, keyboard and speaker won't be defined
|
|
func CreateConfiguredApple() (*Apple2, error) {
|
|
// Get configuration from defaults and the command line
|
|
configuration, err := getConfigurationFromCommandLine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a, err := configure(configuration)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a, nil
|
|
}
|