mirror of
https://github.com/ivanizag/izapple2.git
synced 2026-10-22 00:02:27 +00:00
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.
323 lines
7.9 KiB
Go
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
|
|
}
|