Files
izapple2/setup.go
T
Iván Izaguirre b98cf17696 A libretro core
Runs izapple2 inside RetroArch and the other libretro frontends. It is built
as a C shared library with cgo, so it needs a C toolchain, and it is not part
of the releases.

The frontend owns the frame timing, calling retro_run once per frame, so the
core advances the machine with RunCycles and hands over one video frame and
the matching audio samples on the thread it was called on, with no
goroutines. A frame is 17030 cycles. While a drive asks for fast mode it runs
sixteen frames worth, or loading a disk takes half a minute.

Kept small on purpose: no super hi-res, no Videx video, no RGB card and no
savestates. What is left is generated as 560x192, so the framebuffer is a
fixed 564x192 and the geometry never changes. The cards that produce the
other modes are taken out of the slots.

Diskettes are swapped from the menu through the extended disk control
interface, with the list coming from an M3U playlist, and the saves go to the
save directory of the frontend as overlays.

Go cannot call the function pointers the frontend hands over, so shim.c keeps
them and reaches them through plain C functions, and the callbacks the core
provides go the other way through it. Four entry points take a const pointer,
which cgo cannot generate, so those are wrapped there as well.

The default machine is the new gaming model, an enhanced //e with a
Mockingboard, no mouse and no VidHD, which also serves the command line.

See doc/libretro.md to use it, frontend/a2libretro/README.md for how it works.
2026-08-07 08:53:22 +02:00

323 lines
7.9 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
}
// CreateAppleFromModel builds a machine without reading the command line, for
// the frontends that get their settings elsewhere, like the libretro core.
// The model is the name of a preconfigured model, as returned by
// AvailableModels, or the path to a .cfg file. The overrides replace the
// values of the model, keyed as the command line flags. The filenames are the
// media to load, assigned to the slots as the positional arguments of the
// command line. Video, keyboard and speaker won't be defined.
func CreateAppleFromModel(model string, overrides map[string]string, filenames []string) (*Apple2, error) {
models, _, err := loadConfigurationModelsAndDefault()
if err != nil {
return nil, err
}
overrideValues := newConfiguration()
for key, value := range overrides {
overrideValues.set(key, value)
}
configuration, err := models.getWithOverrides(model, overrideValues)
if err != nil {
return nil, err
}
err = expandSlotConfigurations(configuration)
if err != nil {
return nil, err
}
err = processPositionalFilenames(configuration, filenames)
if err != nil {
return nil, err
}
return configure(configuration)
}
// AvailableModels returns the names of the preconfigured models
func AvailableModels() ([]string, error) {
models, _, err := loadConfigurationModelsAndDefault()
if err != nil {
return nil, err
}
return models.availableModels(), nil
}