Gopher2600/gopher2600.go
2021-04-16 07:57:44 +01:00

960 lines
24 KiB
Go

// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"fmt"
"io"
"math/rand"
"os"
"os/signal"
"strings"
"time"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/debugger"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/debugger/terminal/colorterm"
"github.com/jetsetilly/gopher2600/debugger/terminal/plainterm"
"github.com/jetsetilly/gopher2600/disassembly"
"github.com/jetsetilly/gopher2600/gui"
"github.com/jetsetilly/gopher2600/gui/sdlimgui"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hiscore"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/modalflag"
"github.com/jetsetilly/gopher2600/paths"
"github.com/jetsetilly/gopher2600/performance"
"github.com/jetsetilly/gopher2600/playmode"
"github.com/jetsetilly/gopher2600/recorder"
"github.com/jetsetilly/gopher2600/regression"
"github.com/jetsetilly/gopher2600/statsview"
"github.com/jetsetilly/gopher2600/wavwriter"
)
const defaultInitScript = "debuggerInit"
type stateReq = string
const (
// main thread should end as soon as possible.
//
// takes optional int argument, indicating the status code.
reqQuit stateReq = "QUIT"
// reset interrupt signal handling. used when an alternative
// handler is more appropriate. for example, the playMode and Debugger
// package provide a mode specific handler.
//
// takes no arguments.
reqNoIntSig stateReq = "NOINTSIG"
)
type stateRequest struct {
req stateReq
args interface{}
}
// GuiCreator facilitates the creation, servicing and destruction of GUIs
// that need to be run in the main thread.
//
// Note that there is no Create() function because we need the freedom to
// create the GUI how we want. Instead the creator is a channel which accepts
// a function that returns an instance of GuiCreator.
type GuiCreator interface {
// cleanup resources used by the gui
Destroy(io.Writer)
// Service() should not pause or loop longer than necessary (if at all). It
// MUST ONLY by called as part of a larger loop from the main thread. It
// should service all gui events that are not safe to do in sub-threads.
//
// If the GUI framework does not require this sort of thread safety then
// there is no need for the Service() function to do anything.
Service()
}
// communication between the main() function and the launch() function. this is
// required because many gui solutions (notably SDL) require window event
// handling (including creation) to occur on the main thread.
type mainSync struct {
state chan stateRequest
creator chan func() (GuiCreator, error)
// the result of creator will be returned on either of these two channels.
creation chan GuiCreator
creationError chan error
}
func main() {
sync := &mainSync{
state: make(chan stateRequest),
creator: make(chan func() (GuiCreator, error)),
creation: make(chan GuiCreator),
creationError: make(chan error),
}
// the value to use with os.Exit(). can be changed with reqQuit
// stateRequest
exitVal := 0
// #ctrlc default handler. can be turned off with reqNoIntSig request
intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt)
// launch program as a go routine. further communication is through
// the mainSync instance
go launch(sync)
// loop until done is true. every iteration of the loop we listen for:
//
// 1. interrupt signals
// 2. new gui creation functions
// 3. state requests
// 3. anything in the Service() function of the most recently created GUI
//
done := false
var gui GuiCreator
for !done {
select {
case <-intChan:
fmt.Println("\r")
done = true
case creator := <-sync.creator:
var err error
// destroy existing gui
if gui != nil {
gui.Destroy(os.Stderr)
}
gui, err = creator()
if err != nil {
sync.creationError <- err
// gui is a variable of type interface. nil doesn't work as you
// might expect with interfaces. for instance, even though the
// following outputs "<nil>":
//
// fmt.Println(gui)
//
// the following equation print false:
//
// fmt.Println(gui == nil)
//
// as to the reason why gui does not equal nil, even though
// the creator() function returns nil? well, you tell me.
gui = nil
} else {
sync.creation <- gui
}
case state := <-sync.state:
switch state.req {
case reqQuit:
done = true
if gui != nil {
gui.Destroy(os.Stderr)
}
if state.args != nil {
if v, ok := state.args.(int); ok {
exitVal = v
} else {
panic(fmt.Sprintf("cannot convert %s arguments into int", reqQuit))
}
}
case reqNoIntSig:
signal.Reset(os.Interrupt)
if state.args != nil {
panic(fmt.Sprintf("%s does not accept any arguments", reqNoIntSig))
}
}
default:
// if an instance of gui.Events has been sent to us via sync.events
// then call Service()
if gui != nil {
gui.Service()
}
}
}
fmt.Print("\r")
os.Exit(exitVal)
}
// launch is called from main() as a goroutine. uses mainSync instance to
// indicate gui creation and to quit.
func launch(sync *mainSync) {
// we generate random numbers in some places. seed the generator with the
// current time
rand.Seed(int64(time.Now().Nanosecond()))
md := &modalflag.Modes{Output: os.Stdout}
md.NewArgs(os.Args[1:])
md.NewMode()
md.AddSubModes("RUN", "PLAY", "DEBUG", "DISASM", "PERFORMANCE", "REGRESS", "HISCORE")
p, err := md.Parse()
switch p {
case modalflag.ParseHelp:
sync.state <- stateRequest{req: reqQuit}
return
case modalflag.ParseError:
fmt.Printf("* error: %v\n", err)
// 10
sync.state <- stateRequest{req: reqQuit, args: 10}
return
}
switch md.Mode() {
case "RUN":
fallthrough
case "PLAY":
err = play(md, sync)
case "DEBUG":
err = debug(md, sync)
case "DISASM":
err = disasm(md)
case "PERFORMANCE":
err = perform(md, sync)
case "REGRESS":
err = regress(md, sync)
case "HISCORE":
err = hiscoreServer(md)
}
if err != nil {
fmt.Printf("* error in %s mode: %s\n", md.String(), err)
sync.state <- stateRequest{req: reqQuit, args: 20}
return
}
sync.state <- stateRequest{req: reqQuit}
}
func play(md *modalflag.Modes, sync *mainSync) error {
md.NewMode()
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL")
fullScreen := md.AddBool("fullscreen", false, "start in fullscreen mode")
fpsCap := md.AddBool("fpscap", true, "cap fps to specification")
record := md.AddBool("record", false, "record user input to a file")
wav := md.AddString("wav", "", "record audio to wav file")
patchFile := md.AddString("patch", "", "patch file to apply (cartridge args only)")
hiscore := md.AddBool("hiscore", false, "contact hiscore server [EXPERIMENTAL]")
log := md.AddBool("log", false, "echo debugging log to stdout")
useSavekey := md.AddBool("savekey", false, "use savekey in player 1 port")
profile := md.AddString("profile", "none", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
stats := &[]bool{false}[0]
if statsview.Available() {
stats = md.AddBool("statsview", false, fmt.Sprintf("run stats server (%s)", statsview.Address))
}
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
// set debugging log echo
if *log {
logger.SetEcho(os.Stdout)
} else {
logger.SetEcho(nil)
}
if *stats {
statsview.Launch(os.Stdout)
}
switch len(md.RemainingArgs()) {
case 0:
return fmt.Errorf("2600 cartridge required for %s mode", md)
case 1:
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
defer cartload.Close()
tv, err := television.NewTelevision(*spec)
if err != nil {
return err
}
defer tv.End()
// add wavwriter mixer if wav argument has been specified
if *wav != "" {
aw, err := wavwriter.New(*wav)
if err != nil {
return err
}
tv.AddAudioMixer(aw)
}
// create gui
sync.creator <- func() (GuiCreator, error) {
return sdlimgui.NewSdlImgui(tv)
}
// wait for creator result
var scr gui.GUI
select {
case g := <-sync.creation:
scr = g.(gui.GUI)
case err := <-sync.creationError:
return err
}
// set fps cap
tv.SetFPSCap(*fpsCap)
scr.SetFeature(gui.ReqVSync, *fpsCap)
// set full screen
scr.SetFeature(gui.ReqFullScreen, *fullScreen)
// turn off fallback ctrl-c handling. this so that the playmode can
// end playback recordings gracefully
sync.state <- stateRequest{req: reqNoIntSig}
// check for profiling options
p, err := performance.ParseProfileString(*profile)
if err != nil {
return err
}
// set up a running function
playLaunch := func() error {
err = playmode.Play(tv, scr, *record, cartload, *patchFile, *hiscore, *useSavekey)
if err != nil {
return err
}
return nil
}
if p == performance.ProfileNone {
err = playLaunch()
if err != nil {
return err
}
} else {
// if profile generation has been requested then pass the
// playLaunch() function prepared above, through the RunProfiler()
// function
err := performance.RunProfiler(p, "play", playLaunch)
if err != nil {
return err
}
}
if *record {
fmt.Println("! recording completed")
}
// set ending state
err = scr.SetFeature(gui.ReqState, gui.StateEnding)
if err != nil {
return err
}
default:
return fmt.Errorf("too many arguments for %s mode", md)
}
return nil
}
func debug(md *modalflag.Modes, sync *mainSync) error {
md.NewMode()
defInitScript, err := paths.ResourcePath("", defaultInitScript)
if err != nil {
return err
}
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL")
termType := md.AddString("term", "IMGUI", "terminal type to use in debug mode: IMGUI, COLOR, PLAIN")
initScript := md.AddString("initscript", defInitScript, "script to run on debugger start")
useSavekey := md.AddBool("savekey", false, "use savekey in player 1 port")
profile := md.AddString("profile", "none", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
stats := &[]bool{false}[0]
if statsview.Available() {
stats = md.AddBool("statsview", false, fmt.Sprintf("run stats server (%s)", statsview.Address))
}
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
if *stats {
statsview.Launch(os.Stdout)
}
tv, err := television.NewTelevision(*spec)
if err != nil {
return err
}
defer tv.End()
var term terminal.Terminal
var scr gui.GUI
// create gui
if *termType == "IMGUI" {
sync.creator <- func() (GuiCreator, error) {
return sdlimgui.NewSdlImgui(tv)
}
// wait for creator result
select {
case g := <-sync.creation:
scr = g.(gui.GUI)
case err := <-sync.creationError:
return err
}
// if gui implements the terminal.Broker interface use that terminal
// as a preference
if b, ok := scr.(terminal.Broker); ok {
term = b.GetTerminal()
}
} else {
scr = gui.Stub{}
}
// if the GUI does not supply a terminal then use a color or plain terminal
// as a fallback
if term == nil {
switch strings.ToUpper(*termType) {
default:
fmt.Printf("! unknown terminal type (%s) defaulting to plain\n", *termType)
fallthrough
case "PLAIN":
term = &plainterm.PlainTerminal{}
case "COLOR":
term = &colorterm.ColorTerminal{}
}
}
// turn off fallback ctrl-c handling. this so that the debugger can handle
// quit events with a confirmation request. it also allows the debugger to
// use ctrl-c events to interrupt execution of the emulation without
// quitting the debugger itself
sync.state <- stateRequest{req: reqNoIntSig}
// prepare new debugger instance
dbg, err := debugger.NewDebugger(tv, scr, term, *useSavekey)
if err != nil {
return err
}
switch len(md.RemainingArgs()) {
case 0:
return fmt.Errorf("2600 cartridge required for %s mode", md)
case 1:
// check for profiling options
p, err := performance.ParseProfileString(*profile)
if err != nil {
return err
}
// set up a launch function
dbgLaunch := func() error {
err := dbg.Start(*initScript, cartridgeloader.NewLoader(md.GetArg(0), *mapping))
if err != nil {
return err
}
return nil
}
if p == performance.ProfileNone {
// no profile required so run dbgLaunch() function as normal
err := dbgLaunch()
if err != nil {
return err
}
} else {
// if profile generation has been requested then pass the dbgLaunch()
// function prepared above, through the RunProfiler() function
err := performance.RunProfiler(p, "debugger", dbgLaunch)
if err != nil {
return err
}
}
default:
return fmt.Errorf("too many arguments for %s mode", md)
}
// set ending state
err = scr.SetFeature(gui.ReqState, gui.StateEnding)
if err != nil {
return err
}
return nil
}
func disasm(md *modalflag.Modes) error {
md.NewMode()
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
bytecode := md.AddBool("bytecode", false, "include bytecode in disassembly")
bank := md.AddInt("bank", -1, "show disassembly for a specific bank")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
switch len(md.RemainingArgs()) {
case 0:
return fmt.Errorf("2600 cartridge required for %s mode", md)
case 1:
attr := disassembly.ColumnAttr{
ByteCode: *bytecode,
Label: true,
Cycles: true,
}
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
defer cartload.Close()
dsm, err := disassembly.FromCartridge(cartload)
if err != nil {
// print what disassembly output we do have
if dsm != nil {
// ignore any further errors
_ = dsm.Write(md.Output, attr)
}
return err
}
// output entire disassembly or just a specific bank
if *bank < 0 {
err = dsm.Write(md.Output, attr)
} else {
err = dsm.WriteBank(md.Output, attr, *bank)
}
if err != nil {
return err
}
default:
return fmt.Errorf("too many arguments for %s mode", md)
}
return nil
}
func perform(md *modalflag.Modes, sync *mainSync) error {
md.NewMode()
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL")
display := md.AddBool("display", false, "display TV output")
fpsCap := md.AddBool("fpscap", true, "cap FPS to specification (only valid if -display=true)")
duration := md.AddString("duration", "5s", "run duration (note: there is a 2s overhead)")
profile := md.AddString("profile", "NONE", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
log := md.AddBool("log", false, "echo debugging log to stdout")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
// set debugging log echo
if *log {
logger.SetEcho(os.Stdout)
} else {
logger.SetEcho(nil)
}
switch len(md.RemainingArgs()) {
case 0:
return fmt.Errorf("2600 cartridge required for %s mode", md)
case 1:
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
defer cartload.Close()
tv, err := television.NewTelevision(*spec)
if err != nil {
return err
}
defer tv.End()
// fpscap for tv (see below for gui vsync option)
tv.SetFPSCap(*fpsCap)
// GUI instance if required
var scr gui.GUI
if *display {
// create gui
sync.creator <- func() (GuiCreator, error) {
return sdlimgui.NewSdlImgui(tv)
}
// wait for creator result
select {
case g := <-sync.creation:
scr = g.(gui.GUI)
case err := <-sync.creationError:
return err
}
// fpscap for gui (see above for tv option)
scr.SetFeature(gui.ReqVSync, *fpsCap)
}
// check for profiling options
p, err := performance.ParseProfileString(*profile)
if err != nil {
return err
}
// run performance check
err = performance.Check(md.Output, p, tv, scr, *duration, cartload)
if err != nil {
return err
}
// deliberately not saving gui preferences because we don't want any
// changes to the performance window impacting the play mode
default:
return fmt.Errorf("too many arguments for %s mode", md)
}
return nil
}
func regress(md *modalflag.Modes, sync *mainSync) error {
md.NewMode()
md.AddSubModes("RUN", "LIST", "DELETE", "ADD", "REDUX")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
switch md.Mode() {
case "RUN":
md.NewMode()
// no additional arguments
verbose := md.AddBool("verbose", false, "output more detail (eg. error messages)")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
// turn off default sigint handling
sync.state <- stateRequest{req: reqNoIntSig}
err = regression.RegressRun(md.Output, *verbose, md.RemainingArgs())
if err != nil {
return err
}
case "LIST":
md.NewMode()
// no additional arguments
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
switch len(md.RemainingArgs()) {
case 0:
err := regression.RegressList(md.Output)
if err != nil {
return err
}
default:
return fmt.Errorf("no additional arguments required for %s mode", md)
}
case "DELETE":
md.NewMode()
answerYes := md.AddBool("yes", false, "answer yes to confirmation")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
switch len(md.RemainingArgs()) {
case 0:
return fmt.Errorf("database key required for %s mode", md)
case 1:
// use stdin for confirmation unless "yes" flag has been sent
var confirmation io.Reader
if *answerYes {
confirmation = &yesReader{}
} else {
confirmation = os.Stdin
}
err := regression.RegressDelete(md.Output, confirmation, md.GetArg(0))
if err != nil {
return err
}
default:
return fmt.Errorf("only one entry can be deleted at at time")
}
case "ADD":
return regressAdd(md)
case "REDUX":
md.NewMode()
answerYes := md.AddBool("yes", false, "always answer yes to confirmation")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
var confirmation io.Reader
if *answerYes {
confirmation = &yesReader{}
} else {
confirmation = os.Stdin
}
return regression.RegressRedux(md.Output, confirmation)
}
return nil
}
func regressAdd(md *modalflag.Modes) error {
md.NewMode()
mode := md.AddString("mode", "", "type of regression entry")
notes := md.AddString("notes", "", "additional annotation for the database")
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping [non-playback]")
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL [non-playback]")
numframes := md.AddInt("frames", 10, "number of frames to run [non-playback]")
state := md.AddString("state", "", "record emulator state at every CPU step [non-playback]")
log := md.AddBool("log", false, "echo debugging log to stdout")
md.AdditionalHelp(
`The regression test to be added can be the path to a cartridge file or a previously
recorded playback file. For playback files, the flags marked [non-playback] do not make
sense and will be ignored.
Available modes are VIDEO, PLAYBACK and LOG. If not mode is explicitly given then
VIDEO will be used for ROM files and PLAYBACK will be used for playback recordings.
Value for the -state flag can be one of TV, PORTS, TIMER, CPU and can be used
with the default VIDEO mode.
The -log flag intructs the program to echo the log to the console. Do not confuse this
with the LOG mode. Note that asking for log output will suppress regression progress meters.`)
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
// set debugging log echo
if *log {
logger.SetEcho(os.Stdout)
md.Output = &nopWriter{}
} else {
logger.SetEcho(nil)
}
switch len(md.RemainingArgs()) {
case 0:
return fmt.Errorf("2600 cartridge or playback file required for %s mode", md)
case 1:
var reg regression.Regressor
if *mode == "" {
if err := recorder.IsPlaybackFile(md.GetArg(0)); err == nil {
*mode = "PLAYBACK"
} else if !curated.Is(err, recorder.NotAPlaybackFile) {
return err
} else {
*mode = "VIDEO"
}
}
switch strings.ToUpper(*mode) {
case "VIDEO":
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
defer cartload.Close()
statetype, err := regression.NewStateType(*state)
if err != nil {
return err
}
reg = &regression.VideoRegression{
CartLoad: cartload,
TVtype: strings.ToUpper(*spec),
NumFrames: *numframes,
State: statetype,
Notes: *notes,
}
case "PLAYBACK":
// check and warn if unneeded arguments have been specified
md.Visit(func(flg string) {
if flg == "frames" {
fmt.Printf("! ignored %s flag when adding playback entry\n", flg)
}
})
reg = &regression.PlaybackRegression{
Script: md.GetArg(0),
Notes: *notes,
}
case "LOG":
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
defer cartload.Close()
reg = &regression.LogRegression{
CartLoad: cartload,
TVtype: strings.ToUpper(*spec),
NumFrames: *numframes,
Notes: *notes,
}
}
err := regression.RegressAdd(md.Output, reg)
if err != nil {
// using carriage return (without newline) at beginning of error
// message because we want to overwrite the last output from
// RegressAdd()
return fmt.Errorf("\rerror adding regression test: %v", err)
}
default:
return fmt.Errorf("regression tests can only be added one at a time")
}
return nil
}
func hiscoreServer(md *modalflag.Modes) error {
md.NewMode()
md.AddSubModes("ABOUT", "SETSERVER", "LOGIN", "LOGOFF")
md.AdditionalHelp("Hiscore server support is EXPERIMENTAL")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
switch md.Mode() {
case "ABOUT":
fmt.Println("The hiscore server is experimental and is not currently fully functioning")
case "LOGIN":
md.NewMode()
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
username := ""
args := md.RemainingArgs()
switch len(args) {
case 0:
// an empty string is okay
case 1:
username = args[0]
default:
return fmt.Errorf("too many arguments for %s", md)
}
err = hiscore.Login(os.Stdin, os.Stdout, username)
if err != nil {
return err
}
case "LOGOFF":
err = hiscore.Logoff()
if err != nil {
return err
}
case "SETSERVER":
md.NewMode()
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
server := ""
args := md.RemainingArgs()
switch len(args) {
case 0:
// an empty string is okay
case 1:
server = args[0]
default:
return fmt.Errorf("too many arguments for %s", md)
}
err = hiscore.SetServer(os.Stdin, os.Stdout, server)
if err != nil {
return err
}
}
return nil
}
// nopWriter is an empty writer.
type nopWriter struct{}
func (*nopWriter) Write(p []byte) (n int, err error) {
return 0, nil
}
// yesReader always returns 'y' when it is read.
type yesReader struct{}
func (*yesReader) Read(p []byte) (n int, err error) {
p[0] = 'y'
return 1, nil
}