Files
izapple2/apple2Run.go
Iván Izaguirre 9e11107e4f Let a frontend run the emulation a step at a time
Run and Start own the loop and the timing, which suits the frontends that
let the emulation run free on its own goroutine and sample it whenever they
need to. A frontend that owns the frame timing itself needs the other way
round: advance the machine by a frame and return.

The body of the loop is split into stepInstruction, one 6502 instruction or
one DMA cycle, and drainCommands, which now tells the caller what a command
asks of it instead of reaching into the locals of Start. On top of those,
Init prepares the machine and RunCycles advances it by a number of cycles at
full speed, with no wall clock throttle, from the goroutine of the caller.

Start keeps working exactly as before, it just calls the same pieces.

IsFastModeRequested is exported because the throttle of Start is where fast
mode has its effect: with no throttle to bypass, a frontend pacing the
emulation itself has to give the machine more cycles while a drive asks for
them, or a DOS 3.3 boot takes half a minute.
2026-08-07 08:53:22 +02:00

223 lines
5.5 KiB
Go

package izapple2
import (
"fmt"
"time"
)
const (
// CPUClockMhz is the actual Apple II clock speed
CPUClockMhz = 14.318 / 14
cpuClockEuroMhz = 14.238 / 14
)
const (
maxWaitDuration = 100 * time.Millisecond
cpuSpinLoops = 100
)
// commandOutcome tells the run loop what a drained command requires from it
type commandOutcome int
const (
// commandOutcomeNone when the run loop can continue as usual
commandOutcomeNone commandOutcome = iota
// commandOutcomeKill when the cpu execution loop must stop
commandOutcomeKill
// commandOutcomeResync when the emulation clock reference must be reset
commandOutcomeResync
)
// Run starts the Apple2 emulation
func (a *Apple2) Run() {
a.Start(false)
}
// Init resets the processor and prepares the machine to be run step by
// step with RunCycles. It is not needed with Run or Start, that do it
// themselves.
func (a *Apple2) Init() {
a.cpu.Reset()
a.cycles = a.cpu.GetCycles()
}
// RunCycles advances the emulation by at least the requested number of CPU
// cycles and returns the cycles actually executed, that can be a few more as
// the last instruction is not interrupted. It runs at full speed without the
// wall clock throttle of Start: the caller sets the pace. This is what the
// frontends that own the frame timing, like the libretro core, use instead of
// Start. Init must be called once before the first call, and all the calls
// must be made from the same goroutine.
func (a *Apple2) RunCycles(cycles uint64) uint64 {
a.drainCommands()
start := a.cycles
for a.cycles-start < cycles {
a.stepInstruction()
}
return a.cycles - start
}
// Start the Apple2 emulation, can start paused
func (a *Apple2) Start(paused bool) {
// Start the processor
a.Init()
referenceTime := time.Now()
speedReferenceTime := referenceTime
speedReferenceCycles := uint64(0)
a.paused.Store(paused)
for {
// Run cpu steps
if !a.paused.Load() {
for i := 0; i < cpuSpinLoops; i++ {
a.stepInstruction()
}
if bp := a.cycleBreakpoint.Load(); bp != 0 && a.cycles >= bp {
a.breakPoint.Store(true)
a.cycleBreakpoint.Store(0)
a.paused.Store(true)
}
} else {
time.Sleep(200 * time.Millisecond)
}
// Execute meta commands
switch a.drainCommands() {
case commandOutcomeKill:
return
case commandOutcomeResync:
referenceTime = time.Now()
speedReferenceTime = referenceTime
}
if a.cycleDurationNs != 0 && a.fastRequestsCounter <= 0 {
// Wait until next 6502 step has to run
clockDuration := time.Since(referenceTime)
simulatedDuration := time.Duration(float64(a.cycles) * a.cycleDurationNs)
waitDuration := simulatedDuration - clockDuration
if waitDuration > maxWaitDuration || -waitDuration > maxWaitDuration {
// We have to wait too long or are too much behind. Let's fast forward
referenceTime = referenceTime.Add(-waitDuration)
waitDuration = 0
}
if waitDuration > 0 {
time.Sleep(waitDuration)
}
}
if a.cycles-speedReferenceCycles > 1000000 {
// Calculate speed in MHz every million cycles
newTime := time.Now()
elapsedCycles := float64(a.cycles - speedReferenceCycles)
a.currentFreqMHz = 1000.0 * elapsedCycles / float64(newTime.Sub(speedReferenceTime).Nanoseconds())
speedReferenceTime = newTime
speedReferenceCycles = a.cycles
}
}
}
// stepInstruction executes the next 6502 instruction, or a single DMA cycle
// when a card, like the Z80 Softcard, has taken over the bus
func (a *Apple2) stepInstruction() {
if a.dmaActive {
a.cards[a.dmaSlot].runDMACycle()
a.cycles++
} else {
// Conditional tracing
// pc, _ := a.cpu.GetPCAndSP()
// a.cpu.SetTrace(pc >= 0xc700 && pc < 0xc800)
// Execution
startCycles := a.cpu.GetCycles()
a.cpu.ExecuteInstruction()
a.cycles += a.cpu.GetCycles() - startCycles
}
a.tickCards()
a.executionTrace()
}
// drainCommands executes the commands queued by the frontend and returns what
// the run loop has to do about them
func (a *Apple2) drainCommands() commandOutcome {
outcome := commandOutcomeNone
for {
select {
case command := <-a.commandChannel:
switch command.getId() {
case CommandKill:
return commandOutcomeKill
case CommandPause:
a.paused.Store(true)
case CommandStart:
if a.paused.Load() {
a.paused.Store(false)
outcome = commandOutcomeResync
}
case CommandPauseUnpause:
a.paused.Store(!a.paused.Load())
outcome = commandOutcomeResync
default:
// Execute the other commands
a.executeCommand(command)
}
default:
return outcome
}
}
}
func (a *Apple2) reset() {
a.cpu.Reset()
a.mmu.reset()
for _, c := range a.cards {
if c != nil {
c.reset()
}
}
}
func (a *Apple2) tickCards() {
for _, c := range a.tickerCards {
c.tick()
}
}
func (a *Apple2) executionTrace() {
for _, v := range a.tracers {
v.inspect()
}
}
func (a *Apple2) dumpDebugInfo() {
// See "Apple II Monitors Peeled"
pageZeroSymbols := map[uint16]string{
0x36: "CSWL",
0x37: "CSWH",
0x38: "KSWL",
0x39: "KSWH",
0xe2: "ACJVAFLDL", // Apple Pascal
0xe3: "ACJVAFLDH", // Apple Pascal
0xec: "JVBFOLDL", // Apple Pascal
0xed: "JVBFOLDH", // Apple Pascal
0xee: "JVAFOLDL", // Apple Pascal
0xef: "JVAFOLDH", // Apple Pascal
}
fmt.Printf("Page zero values:\n")
for _, k := range []uint16{0x36, 0x37, 0x38, 0x39, 0xe2, 0xe3, 0xec, 0xed, 0xee, 0xef} {
d := a.mmu.physicalMainRAM.peek(k)
fmt.Printf(" %v(0x%x): 0x%02x\n", pageZeroSymbols[k], k, d)
}
pc := uint16(0xc700)
for pc < 0xc800 {
line, newPc := a.cpu.DisasmInstruction(pc)
fmt.Println(line)
pc = newPc
}
}