More headless commands

This commit is contained in:
Iván Izaguirre 2022-05-10 13:40:17 +02:00
parent 695eaa603b
commit 5b71a131f9
7 changed files with 192 additions and 75 deletions

View File

@ -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)
}
}

View File

@ -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())

View File

@ -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
}

View File

@ -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 {

View File

@ -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))

View File

@ -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 [\]^_

View File

@ -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 <cycles>\n")
} else if cycles, err := strconv.Atoi(parts[1]); err != nil {
fmt.Printf("Usage: run <cycles>\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 <number>")
} else if code, err := strconv.Atoi(parts[1]); err != nil {
fmt.Println("Usage: key <number>")
} 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 <cycles>
Runs the emulator for <cycles> 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 <key>
Queues the key to the emulator. <key> is a decimal number from 0 to 127.
type <string>
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 <filename>
Stores the active screen to <filename> in PNG format as NTSC color.
* pngm <filename>
Same as "png" in monochrome.
* gif <filename> <seconds> <delay>
Stores the running screen to <filename> in GIF format during <seconds> with a <delay> per frame
in 100ths of a second as NTSC color.
If the emulators is stopped. It is run at full speed during <seconds> and the stopped again.
* gifm <filename> <seconds> <delay>
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
}
}
}