diff --git a/apple2.go b/apple2.go index 77853f1..a6ce70f 100644 --- a/apple2.go +++ b/apple2.go @@ -2,6 +2,7 @@ package izapple2 import ( "fmt" + "sync/atomic" "time" "github.com/ivanizag/iz6502" @@ -20,7 +21,9 @@ type Apple2 struct { commandChannel chan int cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz fastMode bool - fastRequestsCounter int + fastRequestsCounter int32 + cycleBreakpoint uint64 + breakPoint bool profile bool showSpeed bool paused bool @@ -45,15 +48,21 @@ const ( // Run starts the Apple2 emulation func (a *Apple2) Run() { + a.Start(false) +} +// Start the Apple2 emulation, can start paused +func (a *Apple2) Start(paused bool) { // Start the processor a.cpu.Reset() referenceTime := time.Now() speedReferenceTime := referenceTime speedReferenceCycles := uint64(0) + a.paused = paused + for { - // Run a 6502 step + // Run 6502 steps if !a.paused { for i := 0; i < cpuSpinLoops; i++ { // Conditional tracing @@ -66,6 +75,12 @@ func (a *Apple2) Run() { // Special tracing a.executionTrace() } + + if a.cycleBreakpoint != 0 && a.cpu.GetCycles() >= a.cycleBreakpoint { + a.breakPoint = true + a.cycleBreakpoint = 0 + a.paused = true + } } else { time.Sleep(200 * time.Millisecond) } @@ -78,7 +93,17 @@ func (a *Apple2) Run() { switch command { case CommandKill: return - case CommandPauseUnpauseEmulator: + case CommandPause: + if !a.paused { + a.paused = true + } + case CommandStart: + if a.paused { + a.paused = false + referenceTime = time.Now() + speedReferenceTime = referenceTime + } + case CommandPauseUnpause: a.paused = !a.paused referenceTime = time.Now() speedReferenceTime = referenceTime @@ -134,6 +159,20 @@ func (a *Apple2) IsPaused() bool { return a.paused } +func (a *Apple2) GetCycles() uint64 { + return a.cpu.GetCycles() +} + +// SetCycleBreakpoint sets a cycle number to pause the emulator. 0 to disable +func (a *Apple2) SetCycleBreakpoint(cycle uint64) { + a.cycleBreakpoint = cycle + a.breakPoint = false +} + +func (a *Apple2) BreakPoint() bool { + return a.breakPoint +} + func (a *Apple2) setProfiling(value bool) { a.profile = value } @@ -168,8 +207,12 @@ const ( CommandKill // CommandReset executes a 6502 reset CommandReset - // CommandPauseUnpauseEmulator allows the Pause button to freeze the emulator for a coffee break - CommandPauseUnpauseEmulator + // CommandPauseUnpause allows the Pause button to freeze the emulator for a coffee break + CommandPauseUnpause + // CommandPause pauses the emulator + CommandPause + // CommandStart restarts the emulator + CommandStart ) // SendCommand enqueues a command to the emulator thread @@ -201,16 +244,16 @@ func (a *Apple2) executeCommand(command int) { } } -func (a *Apple2) requestFastMode() { +func (a *Apple2) RequestFastMode() { // Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain. if a.fastMode { - a.fastRequestsCounter++ + atomic.AddInt32(&a.fastRequestsCounter, 1) } } -func (a *Apple2) releaseFastMode() { +func (a *Apple2) ReleaseFastMode() { if a.fastMode { - a.fastRequestsCounter-- + atomic.AddInt32(&a.fastRequestsCounter, -1) } } diff --git a/cardDisk2.go b/cardDisk2.go index db38b4b..669fca8 100644 --- a/cardDisk2.go +++ b/cardDisk2.go @@ -145,7 +145,7 @@ func (c *CardDisk2) softSwitchQ4(value bool) { if !value && c.power { // Turn off c.power = false - c.a.releaseFastMode() + c.a.ReleaseFastMode() drive := &c.drive[c.selected] if drive.diskette != nil { drive.diskette.PowerOff(c.a.cpu.GetCycles()) @@ -153,7 +153,7 @@ func (c *CardDisk2) softSwitchQ4(value bool) { } else if value && !c.power { // Turn on c.power = true - c.a.requestFastMode() + c.a.RequestFastMode() drive := &c.drive[c.selected] if drive.diskette != nil { drive.diskette.PowerOn(c.a.cpu.GetCycles()) diff --git a/cardFastChip.go b/cardFastChip.go index cf5b5dd..55ccd45 100644 --- a/cardFastChip.go +++ b/cardFastChip.go @@ -89,9 +89,9 @@ func (c *CardFastChip) setSpeed(a *Apple2, value uint8) { return } if newAccelerated { - a.requestFastMode() + a.RequestFastMode() } else { - a.releaseFastMode() + a.ReleaseFastMode() } c.accelerated = newAccelerated } diff --git a/frontend/a2fyne/fyneKeyboard.go b/frontend/a2fyne/fyneKeyboard.go index 988b640..98d8cd5 100644 --- a/frontend/a2fyne/fyneKeyboard.go +++ b/frontend/a2fyne/fyneKeyboard.go @@ -135,7 +135,7 @@ func (k *keyboard) putKey(keyEvent *fyne.KeyEvent) { fmt.Println("Saving snapshot") } //case fyne.KeyPause: - // k.s.a.SendCommand(izapple2.CommandPauseUnpauseEmulator) + // k.s.a.SendCommand(izapple2.CommandPauseUnpause) } if result != 0 { diff --git a/frontend/a2fyne/toolbar.go b/frontend/a2fyne/toolbar.go index facd313..d00e345 100644 --- a/frontend/a2fyne/toolbar.go +++ b/frontend/a2fyne/toolbar.go @@ -21,7 +21,7 @@ func buildCommandToolbar(s *state, icon fyne.Resource, command int) widget.Toolb func buildToolbar(s *state) *widget.Toolbar { tb := widget.NewToolbar() tb.Append(buildCommandToolbar(s, resourceRestartSvg, izapple2.CommandReset)) - tb.Append(buildCommandToolbar(s, resourcePauseSvg, izapple2.CommandPauseUnpauseEmulator)) + tb.Append(buildCommandToolbar(s, resourcePauseSvg, izapple2.CommandPauseUnpause)) tb.Append(buildCommandToolbar(s, resourceFastForwardSvg, izapple2.CommandToggleSpeed)) tb.Append(widget.NewToolbarSeparator()) tb.Append(newToolbarScreen(s)) diff --git a/frontend/a2sdl/sdlKeyboard.go b/frontend/a2sdl/sdlKeyboard.go index ba5ad1d..1c31c6d 100644 --- a/frontend/a2sdl/sdlKeyboard.go +++ b/frontend/a2sdl/sdlKeyboard.go @@ -131,7 +131,7 @@ func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) { fmt.Println("Saving snapshot 'snapshot.png'") } case sdl.K_PAUSE: - k.a.SendCommand(izapple2.CommandPauseUnpauseEmulator) + k.a.SendCommand(izapple2.CommandPauseUnpause) } // Missing values 91 to 95. Usually control for [\]^_ diff --git a/frontend/headless/main.go b/frontend/headless/main.go index cc88466..a3e2c36 100644 --- a/frontend/headless/main.go +++ b/frontend/headless/main.go @@ -5,6 +5,7 @@ import ( "fmt" "image/gif" "os" + "strconv" "strings" "time" @@ -17,11 +18,11 @@ func main() { fe := &headLessFrontend{} fe.keyChannel = make(chan uint8, 200) a.SetKeyboardProvider(fe) - go a.Run() + go a.Start(true /*paused*/) inReader := bufio.NewReader(os.Stdin) - running := true - for running { + done := false + for !done { fmt.Print("* ") text, err := inReader.ReadString('\n') if err != nil { @@ -31,18 +32,65 @@ func main() { parts := strings.Split(text, " ") command := strings.ToLower(parts[0]) switch command { - case "exit": - a.SendCommand(izapple2.CommandKill) - running = false - case "pts": - fallthrough - case "printtextscreen": + // General commands + case "quit": + a.SendCommand(izapple2.CommandKill) + done = true + case "help": + fmt.Print(help) + + // Emulation control commands + case "start": + a.SendCommand(izapple2.CommandStart) + spinWait(func() bool { return !a.IsPaused() }) + case "pause": + a.SendCommand(izapple2.CommandPause) + spinWait(func() bool { return a.IsPaused() }) + case "run": + if len(parts) != 2 { + fmt.Printf("Usage: run \n") + } else if cycles, err := strconv.Atoi(parts[1]); err != nil { + fmt.Printf("Usage: run \n") + } else if !a.IsPaused() { + fmt.Printf("Emulation is already running\n") + } else { + a.RequestFastMode() + a.SetCycleBreakpoint(a.GetCycles() + uint64(cycles)*1000) + a.SendCommand(izapple2.CommandStart) + spinWait(func() bool { return a.BreakPoint() }) + a.ReleaseFastMode() + } + case "cycle": + fmt.Printf("%v\n", a.GetCycles()) + case "reset": + a.SendCommand(izapple2.CommandReset) + + // Keyboard related commands + case "key": + if len(parts) < 2 { + fmt.Println("Usage: key ") + } else if code, err := strconv.Atoi(parts[1]); err != nil { + fmt.Println("Usage: key ") + } else { + fe.putKey(uint8(code)) + } + case "type": + text := strings.Join(parts[1:], " ") + for _, char := range text { + fe.putKey(uint8(char)) + } + case "enter": + fe.putKey(13) + case "clearkeys": + fe.clearKeyQueue() + + //Screen related commands + case "text": fmt.Print(izapple2.DumpTextModeAnsi(a)) - case "ss": - fallthrough - case "savescreen": + // Old: + case "png": err := screen.SaveSnapshot(a, screen.ScreenModeNTSC, "snapshot.png") if err != nil { fmt.Printf("Error saving screen: %v.\n.", err) @@ -50,9 +98,7 @@ func main() { fmt.Println("Saving screen 'snapshot.png'") } - case "ssm": - fallthrough - case "savescreenmono": + case "pngm": err := screen.SaveSnapshot(a, screen.ScreenModePlain, "snapshot.png") if err != nil { fmt.Printf("Error saving screen: %v.\n.", err) @@ -60,59 +106,72 @@ func main() { fmt.Println("Saving screen 'snapshot.png'") } - case "k": - fallthrough - case "key": - if len(parts) < 2 { - fmt.Println("No key specified.") - } else { - key := uint8(parts[1][0]) - fe.keyChannel <- key - } - - case "ks": - fallthrough - case "keys": - text := strings.Join(parts[1:], " ") - for _, char := range text { - fe.keyChannel <- uint8(char) - } - - case "kr": - text := strings.Join(parts[1:], " ") - for _, char := range text { - fe.keyChannel <- uint8(char) - } - fe.keyChannel <- 13 - - case "r": - fallthrough - case "return": - fe.keyChannel <- 13 - case "gif": SaveGif(a, "snapshot.gif") - case "help": - fmt.Print(` -Available commands: - Exit: Stops the emulator and quits - PrintTextScreen or pts: Prints the text mode screen - PrintTextScreen, pts: Prints the text mode screen - SaveScreen or ss: Saves the screen with NTSC colors to "snapshot.png" - SaveScreenMono or ssm: Saves the monochromatic screen to "snapshot.png" - Key or k: Sends a key to the emulator - Keys or ks: Sends a string to the emulator - Return or r: Sends a return to the emulator - GIF or gif: Captures a GIF animation - Help: Prints this help -`) default: fmt.Println("Unknown command.") } } } +var help = ` +General commands: + quit + Quits + help + Prints this help + +Emulation control commands: + start + Runs the emulator + stop + Stops the emulator + run + Runs the emulator for thousand cycles at full speed. Waits until completed. + cycle + Prints the current cycle count + reset + Sends a reset to the emulator + +Keyboard related commands: + key + Queues the key to the emulator. is a decimal number from 0 to 127. + type + Queues the string characters to the emulator. No quotes for the argument, it can have spaces. + enter + Queues the enter key to the emulator. Alias for "key 13". + clearkeys + Clears the key queue. + +Screen related commands: + text + Prints the text mode screen. + * png + Stores the active screen to in PNG format as NTSC color. + * pngm + Same as "png" in monochrome. + * gif + Stores the running screen to in GIF format during with a per frame + in 100ths of a second as NTSC color. + If the emulators is stopped. It is run at full speed during and the stopped again. + * gifm + Same as "gif" in monochrome. + +` + +/* +TODO: + floppy related commands: load disk.... + joystick related commands: set paddle and button state, dump state +*/ + +func spinWait(f func() bool) { + for !f() { + time.Sleep(time.Millisecond * 1) + } +} + func SaveGif(a *izapple2.Apple2, filename string) error { animation := gif.GIF{} @@ -169,3 +228,18 @@ func (fe *headLessFrontend) GetKey(strobed bool) (key uint8, ok bool) { } return } + +func (fe *headLessFrontend) putKey(key uint8) { + fe.keyChannel <- key +} + +func (fe *headLessFrontend) clearKeyQueue() { + empty := false + for !empty { + select { + case <-fe.keyChannel: + default: + empty = true + } + } +}