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 ( import (
"fmt" "fmt"
"sync/atomic"
"time" "time"
"github.com/ivanizag/iz6502" "github.com/ivanizag/iz6502"
@ -20,7 +21,9 @@ type Apple2 struct {
commandChannel chan int commandChannel chan int
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
fastMode bool fastMode bool
fastRequestsCounter int fastRequestsCounter int32
cycleBreakpoint uint64
breakPoint bool
profile bool profile bool
showSpeed bool showSpeed bool
paused bool paused bool
@ -45,15 +48,21 @@ const (
// Run starts the Apple2 emulation // Run starts the Apple2 emulation
func (a *Apple2) Run() { func (a *Apple2) Run() {
a.Start(false)
}
// Start the Apple2 emulation, can start paused
func (a *Apple2) Start(paused bool) {
// Start the processor // Start the processor
a.cpu.Reset() a.cpu.Reset()
referenceTime := time.Now() referenceTime := time.Now()
speedReferenceTime := referenceTime speedReferenceTime := referenceTime
speedReferenceCycles := uint64(0) speedReferenceCycles := uint64(0)
a.paused = paused
for { for {
// Run a 6502 step // Run 6502 steps
if !a.paused { if !a.paused {
for i := 0; i < cpuSpinLoops; i++ { for i := 0; i < cpuSpinLoops; i++ {
// Conditional tracing // Conditional tracing
@ -66,6 +75,12 @@ func (a *Apple2) Run() {
// Special tracing // Special tracing
a.executionTrace() a.executionTrace()
} }
if a.cycleBreakpoint != 0 && a.cpu.GetCycles() >= a.cycleBreakpoint {
a.breakPoint = true
a.cycleBreakpoint = 0
a.paused = true
}
} else { } else {
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
} }
@ -78,7 +93,17 @@ func (a *Apple2) Run() {
switch command { switch command {
case CommandKill: case CommandKill:
return 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 a.paused = !a.paused
referenceTime = time.Now() referenceTime = time.Now()
speedReferenceTime = referenceTime speedReferenceTime = referenceTime
@ -134,6 +159,20 @@ func (a *Apple2) IsPaused() bool {
return a.paused 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) { func (a *Apple2) setProfiling(value bool) {
a.profile = value a.profile = value
} }
@ -168,8 +207,12 @@ const (
CommandKill CommandKill
// CommandReset executes a 6502 reset // CommandReset executes a 6502 reset
CommandReset CommandReset
// CommandPauseUnpauseEmulator allows the Pause button to freeze the emulator for a coffee break // CommandPauseUnpause allows the Pause button to freeze the emulator for a coffee break
CommandPauseUnpauseEmulator CommandPauseUnpause
// CommandPause pauses the emulator
CommandPause
// CommandStart restarts the emulator
CommandStart
) )
// SendCommand enqueues a command to the emulator thread // 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. // Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain.
if a.fastMode { if a.fastMode {
a.fastRequestsCounter++ atomic.AddInt32(&a.fastRequestsCounter, 1)
} }
} }
func (a *Apple2) releaseFastMode() { func (a *Apple2) ReleaseFastMode() {
if a.fastMode { 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 { if !value && c.power {
// Turn off // Turn off
c.power = false c.power = false
c.a.releaseFastMode() c.a.ReleaseFastMode()
drive := &c.drive[c.selected] drive := &c.drive[c.selected]
if drive.diskette != nil { if drive.diskette != nil {
drive.diskette.PowerOff(c.a.cpu.GetCycles()) drive.diskette.PowerOff(c.a.cpu.GetCycles())
@ -153,7 +153,7 @@ func (c *CardDisk2) softSwitchQ4(value bool) {
} else if value && !c.power { } else if value && !c.power {
// Turn on // Turn on
c.power = true c.power = true
c.a.requestFastMode() c.a.RequestFastMode()
drive := &c.drive[c.selected] drive := &c.drive[c.selected]
if drive.diskette != nil { if drive.diskette != nil {
drive.diskette.PowerOn(c.a.cpu.GetCycles()) drive.diskette.PowerOn(c.a.cpu.GetCycles())

View File

@ -89,9 +89,9 @@ func (c *CardFastChip) setSpeed(a *Apple2, value uint8) {
return return
} }
if newAccelerated { if newAccelerated {
a.requestFastMode() a.RequestFastMode()
} else { } else {
a.releaseFastMode() a.ReleaseFastMode()
} }
c.accelerated = newAccelerated c.accelerated = newAccelerated
} }

View File

@ -135,7 +135,7 @@ func (k *keyboard) putKey(keyEvent *fyne.KeyEvent) {
fmt.Println("Saving snapshot") fmt.Println("Saving snapshot")
} }
//case fyne.KeyPause: //case fyne.KeyPause:
// k.s.a.SendCommand(izapple2.CommandPauseUnpauseEmulator) // k.s.a.SendCommand(izapple2.CommandPauseUnpause)
} }
if result != 0 { 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 { func buildToolbar(s *state) *widget.Toolbar {
tb := widget.NewToolbar() tb := widget.NewToolbar()
tb.Append(buildCommandToolbar(s, resourceRestartSvg, izapple2.CommandReset)) 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(buildCommandToolbar(s, resourceFastForwardSvg, izapple2.CommandToggleSpeed))
tb.Append(widget.NewToolbarSeparator()) tb.Append(widget.NewToolbarSeparator())
tb.Append(newToolbarScreen(s)) tb.Append(newToolbarScreen(s))

View File

@ -131,7 +131,7 @@ func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) {
fmt.Println("Saving snapshot 'snapshot.png'") fmt.Println("Saving snapshot 'snapshot.png'")
} }
case sdl.K_PAUSE: case sdl.K_PAUSE:
k.a.SendCommand(izapple2.CommandPauseUnpauseEmulator) k.a.SendCommand(izapple2.CommandPauseUnpause)
} }
// Missing values 91 to 95. Usually control for [\]^_ // Missing values 91 to 95. Usually control for [\]^_

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"image/gif" "image/gif"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@ -17,11 +18,11 @@ func main() {
fe := &headLessFrontend{} fe := &headLessFrontend{}
fe.keyChannel = make(chan uint8, 200) fe.keyChannel = make(chan uint8, 200)
a.SetKeyboardProvider(fe) a.SetKeyboardProvider(fe)
go a.Run() go a.Start(true /*paused*/)
inReader := bufio.NewReader(os.Stdin) inReader := bufio.NewReader(os.Stdin)
running := true done := false
for running { for !done {
fmt.Print("* ") fmt.Print("* ")
text, err := inReader.ReadString('\n') text, err := inReader.ReadString('\n')
if err != nil { if err != nil {
@ -31,18 +32,65 @@ func main() {
parts := strings.Split(text, " ") parts := strings.Split(text, " ")
command := strings.ToLower(parts[0]) command := strings.ToLower(parts[0])
switch command { switch command {
case "exit":
a.SendCommand(izapple2.CommandKill)
running = false
case "pts": // General commands
fallthrough case "quit":
case "printtextscreen": 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)) fmt.Print(izapple2.DumpTextModeAnsi(a))
case "ss": // Old:
fallthrough case "png":
case "savescreen":
err := screen.SaveSnapshot(a, screen.ScreenModeNTSC, "snapshot.png") err := screen.SaveSnapshot(a, screen.ScreenModeNTSC, "snapshot.png")
if err != nil { if err != nil {
fmt.Printf("Error saving screen: %v.\n.", err) fmt.Printf("Error saving screen: %v.\n.", err)
@ -50,9 +98,7 @@ func main() {
fmt.Println("Saving screen 'snapshot.png'") fmt.Println("Saving screen 'snapshot.png'")
} }
case "ssm": case "pngm":
fallthrough
case "savescreenmono":
err := screen.SaveSnapshot(a, screen.ScreenModePlain, "snapshot.png") err := screen.SaveSnapshot(a, screen.ScreenModePlain, "snapshot.png")
if err != nil { if err != nil {
fmt.Printf("Error saving screen: %v.\n.", err) fmt.Printf("Error saving screen: %v.\n.", err)
@ -60,59 +106,72 @@ func main() {
fmt.Println("Saving screen 'snapshot.png'") 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": case "gif":
SaveGif(a, "snapshot.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: default:
fmt.Println("Unknown command.") 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 { func SaveGif(a *izapple2.Apple2, filename string) error {
animation := gif.GIF{} animation := gif.GIF{}
@ -169,3 +228,18 @@ func (fe *headLessFrontend) GetKey(strobed bool) (key uint8, ok bool) {
} }
return 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
}
}
}