Files
izapple2/setup.go
T
Iván Izaguirre 11092ed1d8 Keep the disk images unmodified with a save directory
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.
2026-08-07 08:53:22 +02:00

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
}