input system and ports system separated

playback/recorder and driven input systems moved out of the the ports
package and into a new input package. how the input systems interact has
been clarified and improved - for example, it is now posssible for a
playback file to be used to drive two emulations for comparison purposes

the debugger startup procedure has been clarified with two distinct
startup functions for playmode and debugger - each of which take
different arguments. the clarity has allowed the reintroduction of
recording and playback to the main play mode
This commit is contained in:
JetSetIlly 2021-12-10 14:42:31 +00:00
parent 7e5326fe83
commit 58848acdf9
28 changed files with 771 additions and 614 deletions

View file

@ -30,9 +30,9 @@ type TV interface {
AddAudioMixer(television.AudioMixer)
}
// VCS defines the VCS functions required by a bot.
type VCS interface {
QueueEvent(ports.InputEvent) error
// Input defines the Input functions required by a bot.
type Input interface {
PushEvent(ports.InputEvent) error
}
// Diagnostic instances are sent over the Feedback Diagnostic channel.

View file

@ -152,8 +152,8 @@ func (obs *observer) EndRendering() error {
type videoChessBot struct {
obs *observer
vcs bots.VCS
tv bots.TV
input bots.Input
tv bots.TV
// quit as soon as possible when a value appears on the channel
quit chan bool
@ -364,9 +364,9 @@ func (bot *videoChessBot) moveCursorOnceStep(portid plugging.PortID, direction p
waiting := true
for waiting {
bot.vcs.QueueEvent(ports.InputEvent{Port: portid, Ev: direction, D: ports.DataStickTrue})
bot.input.PushEvent(ports.InputEvent{Port: portid, Ev: direction, D: ports.DataStickTrue})
bot.waitForFrames(downDuration)
bot.vcs.QueueEvent(ports.InputEvent{Port: portid, Ev: direction, D: ports.DataStickFalse})
bot.input.PushEvent(ports.InputEvent{Port: portid, Ev: direction, D: ports.DataStickFalse})
select {
case <-bot.obs.audioFeedback:
waiting = false
@ -462,9 +462,9 @@ func (bot *videoChessBot) moveCursor(moveCol int, moveRow int, shortcut bool) {
waiting := true
for waiting {
bot.vcs.QueueEvent(ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.Fire, D: true})
bot.input.PushEvent(ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.Fire, D: true})
bot.waitForFrames(downDuration)
bot.vcs.QueueEvent(ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.Fire, D: false})
bot.input.PushEvent(ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.Fire, D: false})
select {
case <-bot.obs.audioFeedback:
waiting = false
@ -489,10 +489,10 @@ func (bot *videoChessBot) waitForFrames(n int) {
}
// NewVideoChess creates a new bot able to play chess (via a UCI engine).
func NewVideoChess(vcs bots.VCS, tv bots.TV) (bots.Bot, error) {
func NewVideoChess(vcs bots.Input, tv bots.TV) (bots.Bot, error) {
bot := &videoChessBot{
obs: newObserver(),
vcs: vcs,
input: vcs,
tv: tv,
quit: make(chan bool),
prevPosition: image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines)),

View file

@ -26,17 +26,17 @@ import (
// Bots keeps track of the running bot and handles loading and termination.
type Bots struct {
vcs bots.VCS
tv bots.TV
input bots.Input
tv bots.TV
running bots.Bot
}
// NewBots is the preferred method of initialisation for the Bots type.
func NewBots(vcs bots.VCS, tv bots.TV) *Bots {
func NewBots(input bots.Input, tv bots.TV) *Bots {
return &Bots{
vcs: vcs,
tv: tv,
input: input,
tv: tv,
}
}
@ -48,7 +48,7 @@ func (b *Bots) ActivateBot(cartHash string) (*bots.Feedback, error) {
switch cartHash {
case "043ef523e4fcb9fc2fc2fda21f15671bf8620fc3":
b.running, err = chess.NewVideoChess(b.vcs, b.tv)
b.running, err = chess.NewVideoChess(b.input, b.tv)
if err != nil {
return nil, curated.Errorf("bots: %v", err)
}

View file

@ -98,11 +98,11 @@ func NewComparison(driverVCS *hardware.VCS) (*Comparison, error) {
// synchronise RIOT ports
sync := make(chan ports.TimedInputEvent, 32)
err = cmp.VCS.RIOT.Ports.SynchroniseWithDriver(sync, tv)
err = cmp.VCS.Input.AttachPassenger(sync)
if err != nil {
return nil, curated.Errorf("comparison: %v", err)
}
err = driverVCS.RIOT.Ports.SynchroniseWithPassenger(sync, driverVCS.TV)
err = driverVCS.Input.AttachDriver(sync)
if err != nil {
return nil, curated.Errorf("comparison: %v", err)
}
@ -180,6 +180,7 @@ func (cmp *Comparison) CreateFromLoader(cartload cartridgeloader.Loader) error {
cmp.isEmulating.Store(false)
}()
// not using setup system to attach cartridge. maybe we should?
err := cmp.VCS.AttachCartridge(cartload)
if err != nil {
cmp.driver.quit <- err

View file

@ -1352,55 +1352,55 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
switch strings.ToUpper(arg) {
case "P0":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer0Pro, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer1Pro, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "COL":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelToggleColor, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
case "SET":
arg, _ := tokens.Get()
switch strings.ToUpper(arg) {
case "P0PRO":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: true}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1PRO":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: true}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P0AM":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: false}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1AM":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: false}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "COL":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: true}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "BW":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: false}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
case "HOLD":
arg, _ := tokens.Get()
switch strings.ToUpper(arg) {
case "SELECT":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: true}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "RESET":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: true}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
case "RELEASE":
arg, _ := tokens.Get()
switch strings.ToUpper(arg) {
case "SELECT":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: false}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "RESET":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: false}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
}
@ -1457,10 +1457,10 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
switch n {
case 0:
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: event, D: value}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case 1:
inp := ports.InputEvent{Port: plugging.PortRightPlayer, Ev: event, D: value}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
if err != nil {
@ -1478,18 +1478,18 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
case 0:
if strings.ToUpper(key) == "NONE" {
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadUp, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
} else {
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadDown, D: rune(key[0])}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
case 1:
if strings.ToUpper(key) == "NONE" {
inp := ports.InputEvent{Port: plugging.PortRightPlayer, Ev: ports.KeypadUp, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
} else {
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadDown, D: rune(key[0])}
_, err = dbg.vcs.HandleInputEvent(inp)
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
}

View file

@ -45,8 +45,10 @@ import (
"github.com/jetsetilly/gopher2600/hardware/television/coords"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/prefs"
"github.com/jetsetilly/gopher2600/recorder"
"github.com/jetsetilly/gopher2600/reflection"
"github.com/jetsetilly/gopher2600/reflection/counter"
"github.com/jetsetilly/gopher2600/resources/unique"
"github.com/jetsetilly/gopher2600/rewind"
"github.com/jetsetilly/gopher2600/setup"
"github.com/jetsetilly/gopher2600/tracker"
@ -85,6 +87,10 @@ type Debugger struct {
// sure Close() is called on end
loader *cartridgeloader.Loader
// gameplay recorder/playback
recorder *recorder.Recorder
playback *recorder.Playback
// comparison emulator
comparison *comparison.Comparison
@ -304,7 +310,7 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
// create userinput/controllers handler
dbg.controllers = userinput.NewControllers()
dbg.controllers.AddInputHandler(dbg.vcs)
dbg.controllers.AddInputHandler(dbg.vcs.Input)
// replace player 1 port with savekey
if useSavekey {
@ -315,7 +321,7 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
}
// create bot coordinator
dbg.bots = wrangler.NewBots(dbg.vcs, dbg.vcs.TV)
dbg.bots = wrangler.NewBots(dbg.vcs.Input, dbg.vcs.TV)
// set up debugging interface to memory
dbg.dbgmem = &dbgmem.DbgMem{
@ -400,6 +406,12 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
dbg.vcs.TV.SetFPSCap(fpsCap)
dbg.gui.SetFeature(gui.ReqMonitorSync, fpsCap)
// initialise terminal
err = dbg.term.Initialise()
if err != nil {
return nil, curated.Errorf("debugger: %v", err)
}
return dbg, nil
}
@ -449,10 +461,13 @@ func (dbg *Debugger) setState(state emulation.State) {
// * see setState() comment, although debugger.SetFeature(ReqSetPause) will
// always be "noisy"
func (dbg *Debugger) setStateQuiet(state emulation.State, quiet bool) {
// do not allow comparison emulation in the rewinding state. remove it if
// we ever enter the rewinding state
if state == emulation.Rewinding {
dbg.endPlayback()
dbg.endRecording()
dbg.endComparison()
// it is thought that bots are okay to enter the rewinding state. this
// might not be true in all future cases.
}
dbg.vcs.TV.SetEmulationState(state)
@ -487,6 +502,9 @@ func (dbg *Debugger) setMode(mode emulation.Mode) error {
return nil
}
// don't stop the recording, playback, comparison or bot sub-systems on
// change of mode
// if there is a halting condition that is not allowed in playmode (see
// targets type) then do not change the emulation mode
//
@ -565,10 +583,15 @@ func (dbg *Debugger) setMode(mode emulation.Mode) error {
// End cleans up any resources that may be dangling.
func (dbg *Debugger) end() {
dbg.endPlayback()
dbg.endRecording()
dbg.endComparison()
dbg.bots.Quit()
dbg.vcs.End()
defer dbg.term.CleanUp()
// set ending state
err := dbg.gui.SetFeature(gui.ReqEnd)
if err != nil {
@ -582,57 +605,33 @@ func (dbg *Debugger) end() {
}
}
// Starts the main emulation sequence.
func (dbg *Debugger) Start(mode emulation.Mode, initScript string, cartload cartridgeloader.Loader, comparisonROM string, comparisonPrefs string) error {
// do not allow comparison emulation inside the debugger. it's far too
// complicated running two emulations that must be synced in the debugger
// loop
if mode == emulation.ModeDebugger && comparisonROM != "" {
return curated.Errorf("debugger: cannot run comparison emulation inside the debugger")
}
// StartInDebugMode starts the emulation with the debugger activated.
func (dbg *Debugger) StartInDebugMode(initScript string, filename string, mapping string) error {
// set running flag as early as possible
dbg.running = true
var err error
var cartload cartridgeloader.Loader
defer dbg.end()
err = dbg.start(mode, initScript, cartload, comparisonROM, comparisonPrefs)
if err != nil {
if curated.Has(err, terminal.UserQuit) {
return nil
if filename == "" {
cartload = cartridgeloader.Loader{}
} else {
cartload, err = cartridgeloader.NewLoader(filename, mapping)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
return curated.Errorf("debugger: %v", err)
}
return nil
}
func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cartridgeloader.Loader, comparisonROM string, comparisonPrefs string) error {
// prepare user interface
err := dbg.term.Initialise()
if err != nil {
return curated.Errorf("debugger: %v", err)
}
defer dbg.term.CleanUp()
err = dbg.attachCartridge(cartload)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
err = dbg.startComparison(comparisonROM, comparisonPrefs)
err = dbg.setMode(emulation.ModeDebugger)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
// set mode to the value requested in the function paramenters
err = dbg.setMode(mode)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
// debugger is running
dbg.running = true
// run initialisation script
if initScript != "" {
scr, err := script.RescribeScript(initScript)
if err == nil {
@ -647,6 +646,80 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
}
}
defer dbg.end()
err = dbg.run()
if err != nil {
if curated.Has(err, terminal.UserQuit) {
return nil
}
return curated.Errorf("debugger: %v", err)
}
return nil
}
// StartInPlaymode starts the emulation ready for game-play.
func (dbg *Debugger) StartInPlayMode(filename string, mapping string, record bool, comparisonROM string, comparisonPrefs string) error {
// set running flag as early as possible
dbg.running = true
var err error
var cartload cartridgeloader.Loader
if filename == "" {
cartload = cartridgeloader.Loader{}
} else {
cartload, err = cartridgeloader.NewLoader(filename, mapping)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
}
err = recorder.IsPlaybackFile(filename)
if err != nil {
if !curated.Is(err, recorder.NotAPlaybackFile) {
return curated.Errorf("debugger: %v", err)
}
err = dbg.attachCartridge(cartload)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
if record {
dbg.startRecording(cartload.ShortName())
}
} else {
if record {
return curated.Errorf("debugger: cannot make a new recording using a playback file")
}
dbg.startPlayback(filename)
}
err = dbg.startComparison(comparisonROM, comparisonPrefs)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
err = dbg.setMode(emulation.ModePlay)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
defer dbg.end()
err = dbg.run()
if err != nil {
if curated.Has(err, terminal.UserQuit) {
return nil
}
return curated.Errorf("debugger: %v", err)
}
return nil
}
func (dbg *Debugger) run() error {
// end script recording gracefully. this way we don't have to worry too
// hard about script scribes
defer func() {
@ -660,7 +733,7 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
for dbg.running {
switch dbg.Mode() {
case emulation.ModePlay:
err = dbg.playLoop()
err := dbg.playLoop()
if err != nil {
// if we ever encounter a cartridge ejected error in playmode
// then simply open up the ROM selector
@ -685,7 +758,7 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
return curated.Errorf("emulation state not supported on *start* of debugging loop: %s", dbg.State())
}
err = dbg.inputLoop(dbg.term, false)
err := dbg.inputLoop(dbg.term, false)
if err != nil {
return curated.Errorf("debugger: %v", err)
}
@ -707,7 +780,7 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
// make sure any cartridge loader has been finished with
if dbg.loader != nil {
err = dbg.loader.Close()
err := dbg.loader.Close()
if err != nil {
return curated.Errorf("debugger: %v", err)
}
@ -752,7 +825,9 @@ func (dbg *Debugger) reset(newCartridge bool) error {
// this is the glue that hold the cartridge and disassembly packages together.
// especially important is the repointing of the symbols table in the instance of dbgmem.
func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error) {
// stop any existing comparison emulation and any existing bots
// stop optional sub-systems that shouldn't survive a new cartridge insertion
dbg.endPlayback()
dbg.endRecording()
dbg.endComparison()
dbg.bots.Quit()
@ -913,6 +988,63 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
return nil
}
func (dbg *Debugger) startRecording(cartShortName string) error {
recording := unique.Filename("recording", cartShortName)
var err error
dbg.recorder, err = recorder.NewRecorder(recording, dbg.vcs)
if err != nil {
return err
}
return nil
}
func (dbg *Debugger) endRecording() {
if dbg.recorder == nil {
return
}
defer func() {
dbg.recorder = nil
}()
err := dbg.recorder.End()
if err != nil {
logger.Logf("debugger", err.Error())
}
}
func (dbg *Debugger) startPlayback(filename string) error {
plb, err := recorder.NewPlayback(filename)
if err != nil {
return err
}
err = dbg.attachCartridge(plb.CartLoad)
if err != nil {
return err
}
err = plb.AttachToVCSInput(dbg.vcs)
if err != nil {
return err
}
dbg.playback = plb
return nil
}
func (dbg *Debugger) endPlayback() {
if dbg.playback == nil {
return
}
dbg.playback = nil
dbg.vcs.Input.AttachPlayback(nil)
}
func (dbg *Debugger) startComparison(comparisonROM string, comparisonPrefs string) error {
if comparisonROM == "" {
return nil

View file

@ -20,7 +20,6 @@ import (
"testing"
"time"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/debugger"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/emulation"
@ -156,7 +155,7 @@ func TestDebugger_withNonExistantInitScript(t *testing.T) {
go trm.testSequence()
err = dbg.Start(emulation.ModeDebugger, "non_existent_script", cartridgeloader.Loader{}, "", "")
err = dbg.StartInDebugMode("", "", "")
if err != nil {
t.Fatalf(err.Error())
}
@ -179,7 +178,7 @@ func TestDebugger(t *testing.T) {
go trm.testSequence()
err = dbg.Start(emulation.ModeDebugger, "", cartridgeloader.Loader{}, "", "")
err = dbg.StartInDebugMode("", "", "")
if err != nil {
t.Fatalf(err.Error())
}

View file

@ -44,8 +44,6 @@ import (
"github.com/jetsetilly/gopher2600/statsview"
)
const defaultInitScript = "debuggerInit"
// communication between the main goroutine and the launch goroutine.
type mainSync struct {
state chan stateRequest
@ -267,145 +265,7 @@ func launch(sync *mainSync) {
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, PAL60")
// 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")
// multiload := md.AddInt("multiload", -1, "force multiload byte (supercharger only; 0 to 255)")
// 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)
// }
// var cartload cartridgeloader.Loader
// switch len(md.RemainingArgs()) {
// case 0:
// // allow loading from file requester
// case 1:
// cartload, err = cartridgeloader.NewLoader(md.GetArg(0), *mapping)
// if err != nil {
// return err
// }
// defer cartload.Close()
// default:
// return fmt.Errorf("too many arguments for %s mode", md)
// }
// 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.state <- stateRequest{req: reqCreateGUI,
// args: guiCreate(func() (guiControl, error) {
// return sdlimgui.NewSdlImgui(tv)
// }),
// }
// // wait for creator result
// var scr gui.GUI
// select {
// case g := <-sync.gui:
// scr = g.(gui.GUI)
// case err := <-sync.guiError:
// return err
// }
// // set fps cap
// tv.SetFPSCap(*fpsCap)
// scr.SetFeature(gui.ReqMonitorSync, *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
// o, 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, *multiload)
// if err != nil {
// return err
// }
// return nil
// }
// if o == 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(o, "play", playLaunch)
// if err != nil {
// return err
// }
// }
// if *record {
// fmt.Println("! recording completed")
// }
// // set ending state
// err = scr.SetFeature(gui.ReqEnd)
// if err != nil {
// return err
// }
// return nil
// }
const defaultInitScript = "debuggerInit"
func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync) error {
md.NewMode()
@ -418,30 +278,49 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL, PAL60")
fpsCap := md.AddBool("fpscap", true, "cap fps to TV specification")
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")
log := md.AddBool("log", false, "echo debugging log to stdout")
termType := md.AddString("term", "IMGUI", "terminal type to use in debug mode: IMGUI, COLOR, PLAIN")
// some arguments are mode specific
// 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]")
// multiload := md.AddInt("multiload", -1, "force multiload byte (supercharger only; 0 to 255)")
// playmode specific arguments
var comparisonROM *string
var comparisonPrefs *string
var record *bool
if emulationMode == emulation.ModePlay {
comparisonROM = md.AddString("comparisonROM", "", "ROM to run in parallel for comparison")
comparisonPrefs = md.AddString("comparisonPrefs", "", "preferences for comparison emulation")
record = md.AddBool("record", false, "record user input to a file")
}
stats := &[]bool{false}[0]
// debugger specific arguments
var initScript *string
if emulationMode == emulation.ModeDebugger {
initScript = md.AddString("initscript", defInitScript, "script to run on debugger start")
}
// statsview if available
var stats *bool
if statsview.Available() {
stats = md.AddBool("statsview", false, fmt.Sprintf("run stats server (%s)", statsview.Address))
}
// parse arguments
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
// check remaining arguments
if len(md.RemainingArgs()) > 1 {
return fmt.Errorf("too many arguments for %s mode", md)
}
// set debugging log echo
if *log {
logger.SetEcho(os.Stdout)
@ -449,7 +328,7 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
logger.SetEcho(nil)
}
if *stats {
if stats != nil && *stats {
statsview.Launch(os.Stdout)
}
@ -460,6 +339,7 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
// turning the emulation's interrupt handler off
sync.state <- stateRequest{req: reqNoIntSig}
// GUI create function
create := func(e emulation.Emulation) (gui.GUI, terminal.Terminal, error) {
var term terminal.Terminal
var scr gui.GUI
@ -512,23 +392,6 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
return err
}
var cartload cartridgeloader.Loader
switch len(md.RemainingArgs()) {
case 0:
// allow launch with no ROM specified on command line
case 1:
// cartridge loader. note that there is no deferred cartload.Close(). the
// debugger type itself will handle this.
cartload, err = cartridgeloader.NewLoader(md.GetArg(0), *mapping)
if err != nil {
return err
}
default:
return fmt.Errorf("too many arguments for %s mode", md)
}
// check for profiling options
prf, err := performance.ParseProfileString(*profile)
if err != nil {
@ -537,23 +400,21 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
// set up a launch function
dbgLaunch := func() error {
// check if comparison was defined and dereference if it was, otherwise
// comp is just the empty string
var comp string
if comparisonROM != nil {
comp = *comparisonROM
switch emulationMode {
case emulation.ModeDebugger:
err := dbg.StartInDebugMode(*initScript, md.GetArg(0), *mapping)
if err != nil {
return err
}
case emulation.ModePlay:
err := dbg.StartInPlayMode(md.GetArg(0), *mapping, *record, *comparisonROM, *comparisonPrefs)
if err != nil {
return err
}
}
// same for compPrefs
var compPrefs string
if comparisonPrefs != nil {
compPrefs = *comparisonPrefs
}
err := dbg.Start(emulationMode, *initScript, cartload, comp, compPrefs)
if err != nil {
return err
}
return nil
}

52
hardware/input/doc.go Normal file
View file

@ -0,0 +1,52 @@
// 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/>.
// Pacakge input coordinates all the different types of input into the VCS.
// The types of input handled by the package include:
//
// 1) Immediate input from the user (see userinput package)
// 2) Playback for a previously recorded script (see recorder package)
// 3) Driven events from a driver emulation (see below))
// 4) Pushed events
//
// In addition it also coordinates the passing of input to other packages that
// need to know about an input event.
//
// 1) The RIOT ports (see Ports package)
// 2) Events to be recorded to a playback script (see recorder package)
// 3) Events to be driven to a passenger emulation (see below)
//
// The input package will handle impossible situations are return an error when
// appropriate. For example, it is not possible for an emulation to be a
// playback and a recorder at the same time.
//
// Points 1 in both lists is the normal type of input you would expect in an
// emultor that allows people to play games and as such, won't be discussed
// further.
//
// Points 2 in the lists is well covered by the recorder package.
//
// Points 3 in both lists above refer to driven events and driver & passenger
// emulations. This system is a way os synchronising two emulations such that
// the input of one drives the input of the other. The input package ensures
// that input events occur in bothe emulations at the same time - time being
// measure by the coordinates of the TV instances attached to the emulations.
//
// For an example of a driven emulation see the comparison package.
//
// Lastly, pushed events, as refered to in point 4, are events that have
// arrived from a different goroutine. For an example of pushed events see the
// various bot implementations in the bots package.
package input

87
hardware/input/driven.go Normal file
View file

@ -0,0 +1,87 @@
// 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 input
import (
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
)
// handleDrivenEvents checks for input driven from a another emulation.
func (inp *Input) handleDrivenEvents() error {
if inp.checkForDriven {
ev := inp.drivenInputEvent
done := false
for !done {
c := inp.tv.GetCoords()
if coords.Equal(c, ev.Time) {
_, err := inp.ports.HandleInputEvent(ev.InputEvent)
if err != nil {
return err
}
} else if coords.GreaterThan(c, ev.Time) {
return curated.Errorf("input: driven input seen too late. emulations not synced correctly.")
} else {
return nil
}
select {
case inp.drivenInputEvent = <-inp.fromDriver:
if inp.checkForDriven {
curated.Errorf("input: driven input received before previous input was processed")
}
default:
done = true
inp.checkForDriven = false
}
ev = inp.drivenInputEvent
}
}
if inp.fromDriver != nil {
select {
case inp.drivenInputEvent = <-inp.fromDriver:
if inp.checkForDriven {
curated.Errorf("input: driven input received before previous input was processed")
}
inp.checkForDriven = true
default:
}
}
return nil
}
// AttachPassenger should be called by an emulation that wants to be driven by another emulation.
func (inp *Input) AttachPassenger(driver chan ports.TimedInputEvent) error {
if inp.toPassenger != nil {
return curated.Errorf("input: attach passenger: emulation already defined as an input driver")
}
inp.fromDriver = driver
inp.setProcessFunc()
return nil
}
// AttachDriver should be called by an emulation that is prepared to drive another emulation.
func (inp *Input) AttachDriver(passenger chan ports.TimedInputEvent) error {
if inp.fromDriver != nil {
return curated.Errorf("input: attach driver: emulation already defined as being an input passenger")
}
inp.toPassenger = passenger
return nil
}

142
hardware/input/input.go Normal file
View file

@ -0,0 +1,142 @@
// 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 input
import (
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
)
// TV defines the television functions required by the Input system.
type TV interface {
GetCoords() coords.TelevisionCoords
}
// Input handles all forms of input into the VCS.
type Input struct {
tv TV
ports *ports.Ports
playback EventPlayback
recorder EventRecorder
// events pushed onto the input queue
pushed chan ports.InputEvent
// the following fields all relate to driven input, for either the driver
// or for the passenger (the driven)
fromDriver chan ports.TimedInputEvent
toPassenger chan ports.TimedInputEvent
checkForDriven bool
drivenInputEvent ports.TimedInputEvent
// Process function should be called every VCS step
Process func() error
}
func NewInput(tv TV, p *ports.Ports) *Input {
inp := &Input{
tv: tv,
ports: p,
pushed: make(chan ports.InputEvent, 64),
}
inp.setProcessFunc()
return inp
}
// Plumb a new ports instances into the Input.
func (inp *Input) Plumb(ports *ports.Ports) {
inp.ports = ports
}
// PeripheralID forwards a request of the PeripheralID of the PortID to VCS Ports.
func (inp *Input) PeripheralID(id plugging.PortID) plugging.PeripheralID {
return inp.ports.PeripheralID(id)
}
// HandleInputEvent forwards an input event to VCS Ports.
func (inp *Input) HandleInputEvent(ev ports.InputEvent) (bool, error) {
if inp.recorder != nil {
err := inp.recorder.RecordEvent(ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev})
if err != nil {
return false, err
}
}
handled, err := inp.ports.HandleInputEvent(ev)
if err != nil {
return handled, err
}
// forward to passenger if one is defined
if handled && inp.toPassenger != nil {
select {
case inp.toPassenger <- ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev}:
default:
return handled, curated.Errorf("input: passenger event queue is full: input dropped")
}
}
return handled, nil
}
func (inp *Input) setProcessFunc() {
if inp.fromDriver != nil && inp.playback != nil {
inp.Process = func() error {
if err := inp.handlePushedEvents(); err != nil {
return err
}
if err := inp.handlePlaybackEvents(); err != nil {
return err
}
if err := inp.handleDrivenEvents(); err != nil {
return err
}
return nil
}
return
}
if inp.fromDriver != nil {
inp.Process = func() error {
if err := inp.handlePushedEvents(); err != nil {
return err
}
if err := inp.handleDrivenEvents(); err != nil {
return err
}
return nil
}
return
}
if inp.playback != nil {
inp.Process = func() error {
if err := inp.handlePushedEvents(); err != nil {
return err
}
if err := inp.handlePlaybackEvents(); err != nil {
return err
}
return nil
}
return
}
inp.Process = inp.handlePushedEvents
}

48
hardware/input/pushed.go Normal file
View file

@ -0,0 +1,48 @@
// 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 input
import (
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
)
// PushEvent pushes an InputEvent onto the queue. Will drop the event and
// return an error if queue is full.
func (inp *Input) PushEvent(ev ports.InputEvent) error {
select {
case inp.pushed <- ev:
default:
return curated.Errorf("ports: pushed event queue is full: input dropped")
}
return nil
}
func (inp *Input) handlePushedEvents() error {
done := false
for !done {
select {
case ev := <-inp.pushed:
_, err := inp.ports.HandleInputEvent(ev)
if err != nil {
return err
}
default:
done = true
}
}
return nil
}

104
hardware/input/recording.go Normal file
View file

@ -0,0 +1,104 @@
// 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 input
import (
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
)
// Playback implementations feed controller Events to the device on request
// with the CheckInput() function.
//
// Intended for playback of controller events previously recorded to a file on
// disk but usable for many purposes I suspect. For example, AI control.
type EventPlayback interface {
// note the type restrictions on EventData in the type definition's
// commentary
GetPlayback() (ports.TimedInputEvent, error)
}
// EventRecorder implementations mirror an incoming event.
//
// Implementations should be able to handle being attached to more than one
// peripheral at once. The ID parameter of the EventRecord() function will help
// to differentiate between multiple devices.
type EventRecorder interface {
RecordEvent(ports.TimedInputEvent) error
}
// AttachEventRecorder attaches an EventRecorder implementation.
func (inp *Input) AttachRecorder(r EventRecorder) error {
if inp.playback != nil {
return curated.Errorf("input: attach recorder: emulator already has a playback attached")
}
inp.recorder = r
return nil
}
// AttachPlayback attaches an EventPlayback implementation to the Input
// sub-system. EventPlayback can be nil in order to remove the playback.
func (inp *Input) AttachPlayback(pb EventPlayback) error {
if inp.recorder != nil {
return curated.Errorf("input: attach playback: emulator already has a recorder attached")
}
inp.playback = pb
inp.setProcessFunc()
return nil
}
// handlePlaybackEvents requests playback events from all attached and eligible peripherals.
func (inp *Input) handlePlaybackEvents() error {
if inp.playback == nil {
return nil
}
// loop with GetPlayback() until we encounter a NoPortID or NoEvent
// condition. there might be more than one entry for a particular
// frame/scanline/horizpas state so we need to make sure we've processed
// them all.
//
// this happens in particular with recordings that were made of ROMs with
// panel setup configurations (see setup package) - where the switches are
// set when the TV state is at fr=0 sl=0 cl=0
morePlayback := true
for morePlayback {
ev, err := inp.playback.GetPlayback()
if err != nil {
return err
}
morePlayback = ev.Port != plugging.PortUnplugged && ev.Ev != ports.NoEvent
if morePlayback {
_, err := inp.ports.HandleInputEvent(ev.InputEvent)
if err != nil {
return err
}
// forward to passenger if necessary
if inp.toPassenger != nil {
select {
case inp.toPassenger <- ev:
default:
return curated.Errorf("input: passenger event queue is full: input dropped")
}
}
}
}
return nil
}

View file

@ -80,7 +80,7 @@ func (mem *Memory) Snapshot() *Memory {
return &n
}
// Plumb makes sure everythin is ship-shape after a rewind event.
// Plumb makes sure everything is ship-shape after a rewind event.
func (mem *Memory) Plumb(fromDifferentEmulation bool) {
mem.Cart.Plumb(fromDifferentEmulation)
}

View file

@ -16,17 +16,12 @@
// Package ports represents the input/output parts of the VCS (the IO in
// RIOT).
//
// Emulated peripherals are plugged into the VCS with the Plug() function.
// Input from "real" devices is handled by HandleEvent() which passes the event
// to peripherals in the specified PortID.
//
// Emulations can share user input through the DrivenEvent mechanism. The driver
// emulation should call SynchroniseWithPassenger() and the passenger emulation
// should call SynchroniseWithDriver().
// HandleEvent() should probably no be called directly but instead only through
// the input package
//
// With the DrivenEvent mechanism, the driver sends events to the passenger.
// Both emulations will receive the same user input at the same time, relative
// to television coordinates, so it is important that the driver is running
// ahead of the passenger at all time. See comparison package for model
// implementation.
// Emulated peripherals are plugged into the VCS with the Plug() function.
// Plugging events can be monitored with the plugging.PlugMonitor interface.
package ports

View file

@ -117,23 +117,3 @@ type TimedInputEvent struct {
Time coords.TelevisionCoords
InputEvent
}
// Playback implementations feed controller Events to the device on request
// with the CheckInput() function.
//
// Intended for playback of controller events previously recorded to a file on
// disk but usable for many purposes I suspect. For example, AI control.
type EventPlayback interface {
// note the type restrictions on EventData in the type definition's
// commentary
GetPlayback() (TimedInputEvent, error)
}
// EventRecorder implementations mirror an incoming event.
//
// Implementations should be able to handle being attached to more than one
// peripheral at once. The ID parameter of the EventRecord() function will help
// to differentiate between multiple devices.
type EventRecorder interface {
RecordEvent(TimedInputEvent) error
}

View file

@ -1,187 +0,0 @@
// 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 ports
import (
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
)
func (p *Ports) HandleInputEvents() error {
err := p.handlePushedEvents()
if err != nil {
return err
}
err = p.handleDrivenEvents()
if err != nil {
return err
}
err = p.handlePlaybackEvents()
if err != nil {
return err
}
return nil
}
func (p *Ports) handlePushedEvents() error {
done := false
for !done {
select {
case inp := <-p.pushed:
_, err := p.HandleInputEvent(inp)
if err != nil {
return err
}
default:
done = true
}
}
return nil
}
// handleDrivenEvents checks for input driven from a another emulation.
func (p *Ports) handleDrivenEvents() error {
if p.checkForDriven {
inp := p.drivenInputData
done := false
for !done {
c := p.tv.GetCoords()
if coords.Equal(c, inp.Time) {
_, err := p.HandleInputEvent(inp.InputEvent)
if err != nil {
return err
}
} else if coords.GreaterThan(c, inp.Time) {
return curated.Errorf("ports: driven input seen too late. emulations not synced correctly.")
} else {
return nil
}
select {
case p.drivenInputData = <-p.fromDriver:
if p.checkForDriven {
curated.Errorf("ports: driven input received before previous input was processed")
}
default:
done = true
p.checkForDriven = false
}
inp = p.drivenInputData
}
}
if p.fromDriver != nil {
select {
case p.drivenInputData = <-p.fromDriver:
if p.checkForDriven {
curated.Errorf("ports: driven input received before previous input was processed")
}
p.checkForDriven = true
default:
}
}
return nil
}
// handlePlaybackEvents requests playback events from all attached and eligible peripherals.
func (p *Ports) handlePlaybackEvents() error {
if p.playback == nil {
return nil
}
// loop with GetPlayback() until we encounter a NoPortID or NoEvent
// condition. there might be more than one entry for a particular
// frame/scanline/horizpas state so we need to make sure we've processed
// them all.
//
// this happens in particular with recordings that were made of ROMs with
// panel setup configurations (see setup package) - where the switches are
// set when the TV state is at fr=0 sl=0 cl=0
morePlayback := true
for morePlayback {
inp, err := p.playback.GetPlayback()
if err != nil {
return err
}
morePlayback = inp.Port != plugging.PortUnplugged && inp.Ev != NoEvent
if morePlayback {
_, err := p.HandleInputEvent(inp.InputEvent)
if err != nil {
return err
}
}
}
return nil
}
// HandleInputEvent should only be used from the same gorouine as the
// emulation. Events should be queued with QueueEvent() otherwise.
//
// Consider using HandleInputEvent() function in the VCS type rather than this
// function directly.
func (p *Ports) HandleInputEvent(inp InputEvent) (bool, error) {
var handled bool
var err error
switch inp.Port {
case plugging.PortPanel:
handled, err = p.Panel.HandleEvent(inp.Ev, inp.D)
case plugging.PortLeftPlayer:
handled, err = p.LeftPlayer.HandleEvent(inp.Ev, inp.D)
case plugging.PortRightPlayer:
handled, err = p.RightPlayer.HandleEvent(inp.Ev, inp.D)
}
// forward to passenger if one is defined
if handled && p.toPassenger != nil {
select {
case p.toPassenger <- TimedInputEvent{Time: p.tv.GetCoords(), InputEvent: inp}:
default:
return handled, curated.Errorf("ports: passenger event queue is full: input dropped")
}
}
// if error was because of an unhandled event then return without error
if err != nil {
return handled, curated.Errorf("ports: %v", err)
}
// record event with the EventRecorder
for _, r := range p.recorder {
return handled, r.RecordEvent(TimedInputEvent{Time: p.tv.GetCoords(), InputEvent: inp})
}
return handled, nil
}
// QueueEvent pushes an InputEvent onto the queue. Will drop the event and
// return an error if queue is full.
func (p *Ports) QueueEvent(inp InputEvent) error {
select {
case p.pushed <- inp:
default:
return curated.Errorf("ports: pushed event queue is full: input dropped")
}
return nil
}

View file

@ -23,14 +23,8 @@ import (
"github.com/jetsetilly/gopher2600/hardware/memory/addresses"
"github.com/jetsetilly/gopher2600/hardware/memory/bus"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
)
// TV defines the television functions required by the Ports type.
type TV interface {
GetCoords() coords.TelevisionCoords
}
// Input implements the input/output part of the RIOT (the IO in RIOT).
type Ports struct {
riot bus.ChipBus
@ -40,9 +34,6 @@ type Ports struct {
LeftPlayer Peripheral
RightPlayer Peripheral
playback EventPlayback
recorder []EventRecorder
monitor plugging.PlugMonitor
// local copies of key chip memory registers
@ -84,21 +75,6 @@ type Ports struct {
//
// we use it to mux the Player0 and Player 1 nibbles into the single register
swchaMux uint8
// events pushed onto the input queue
pushed chan InputEvent
// the following fields all relate to driven input, for either the driver
// or for the passenger (the driven)
fromDriver chan TimedInputEvent
toPassenger chan TimedInputEvent
checkForDriven bool
drivenInputData TimedInputEvent
// the time of driven events are measured by television coordinates
//
// not used except to synchronise driver and passenger emulations
tv TV
}
// NewPorts is the preferred method of initialisation of the Ports type.
@ -106,11 +82,9 @@ func NewPorts(riotMem bus.ChipBus, tiaMem bus.ChipBus) *Ports {
p := &Ports{
riot: riotMem,
tia: tiaMem,
recorder: make([]EventRecorder, 0),
swchaFromCPU: 0x00,
swacnt: 0x00,
latch: false,
pushed: make(chan InputEvent, 64),
}
return p
}
@ -267,45 +241,6 @@ func (p *Ports) Step() {
p.Panel.Step()
}
// SynchroniseWithDriver implies that the emulation will receive driven events
// from another emulation.
func (p *Ports) SynchroniseWithDriver(driver chan TimedInputEvent, tv TV) error {
if p.toPassenger != nil {
return curated.Errorf("ports: cannot sync with driver: emulation already defined as a driver of input")
}
if p.playback != nil {
return curated.Errorf("ports: cannot sync with driver: emulation is already receiving input from a playback")
}
p.tv = tv
p.fromDriver = driver
return nil
}
// SynchroniseWithPassenger connects the emulation to a second emulation (the
// passenger) to which user input events will be "driven".
func (p *Ports) SynchroniseWithPassenger(passenger chan TimedInputEvent, tv TV) error {
if p.fromDriver != nil {
return curated.Errorf("ports: cannot sync with passenger: emulation already defined as being driven")
}
p.tv = tv
p.toPassenger = passenger
return nil
}
// AttachPlayback attaches an EventPlayback implementation.
func (p *Ports) AttachPlayback(b EventPlayback) error {
if p.fromDriver != nil {
return curated.Errorf("ports: cannot attach playback: emulation already defined as being driven")
}
p.playback = b
return nil
}
// AttachEventRecorder attaches an EventRecorder implementation.
func (p *Ports) AttachEventRecorder(r EventRecorder) {
p.recorder = append(p.recorder, r)
}
// AttchPlugMonitor implements the plugging.Monitorable interface.
func (p *Ports) AttachPlugMonitor(m plugging.PlugMonitor) {
p.monitor = m
@ -328,10 +263,7 @@ func (p *Ports) AttachPlugMonitor(m plugging.PlugMonitor) {
}
}
// PeripheralID implements userinput.HandleInput interface.
//
// Consider using PeripheralID() function in the VCS type rather than this
// function directly.
// PeripheralID returns the ID of the peripheral in the identified port.
func (p *Ports) PeripheralID(id plugging.PortID) plugging.PeripheralID {
switch id {
case plugging.PortPanel:
@ -376,3 +308,26 @@ func (p *Ports) WriteINPTx(inptx addresses.ChipRegister, data uint8) {
p.tia.ChipWrite(inptx, data)
}
}
// HandleInputEvent forwards the InputEvent to the perupheral in the correct
// port. Returns true if the event was handled and false if not.
func (p *Ports) HandleInputEvent(inp InputEvent) (bool, error) {
var handled bool
var err error
switch inp.Port {
case plugging.PortPanel:
handled, err = p.Panel.HandleEvent(inp.Ev, inp.D)
case plugging.PortLeftPlayer:
handled, err = p.LeftPlayer.HandleEvent(inp.Ev, inp.D)
case plugging.PortRightPlayer:
handled, err = p.RightPlayer.HandleEvent(inp.Ev, inp.D)
}
// if error was because of an unhandled event then return without error
if err != nil {
return handled, curated.Errorf("ports: %v", err)
}
return handled, nil
}

View file

@ -50,7 +50,7 @@ func (vcs *VCS) Run(continueCheck func() (emulation.State, error)) error {
// see the equivalient videoCycle() in the VCS.Step() function for an
// explanation for what's going on here:
videoCycle := func() error {
if err := vcs.RIOT.Ports.HandleInputEvents(); err != nil {
if err := vcs.Input.Process(); err != nil {
return err
}

View file

@ -57,7 +57,7 @@ func (vcs *VCS) Step(videoCycleCallback func() error) error {
// I don't believe any visual or audible artefacts of the VCS (undocumented
// or not) rely on the details of the CPU-TIA relationship.
videoCycle := func() error {
if err := vcs.RIOT.Ports.HandleInputEvents(); err != nil {
if err := vcs.Input.Process(); err != nil {
return err
}

View file

@ -19,12 +19,12 @@ import (
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/hardware/input"
"github.com/jetsetilly/gopher2600/hardware/instance"
"github.com/jetsetilly/gopher2600/hardware/memory"
"github.com/jetsetilly/gopher2600/hardware/memory/addresses"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge"
"github.com/jetsetilly/gopher2600/hardware/riot"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/controllers"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/panel"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
@ -50,6 +50,8 @@ type VCS struct {
RIOT *riot.RIOT
TIA *tia.TIA
Input *input.Input
// The Clock defines the basic speed at which the the machine is runningt. This governs
// the speed of the CPU, the RIOT and attached peripherals. The TIA runs at
// exactly three times this speed.
@ -90,6 +92,8 @@ func NewVCS(tv *television.Television) (*VCS, error) {
vcs.CPU = cpu.NewCPU(vcs.Instance, vcs.Mem)
vcs.RIOT = riot.NewRIOT(vcs.Instance, vcs.Mem.RIOT, vcs.Mem.TIA)
vcs.Input = input.NewInput(vcs.TV, vcs.RIOT.Ports)
vcs.TIA, err = tia.NewTIA(vcs.Instance, vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
if err != nil {
return nil, err
@ -120,6 +124,21 @@ func (vcs *VCS) End() {
vcs.TV.End()
}
// Plumb the various VCS sub-systems together after a rewind.
func (vcs *VCS) Plumb(fromDifferentEmulation bool) {
vcs.CPU.Plumb(vcs.Mem)
vcs.Mem.Plumb(fromDifferentEmulation)
vcs.RIOT.Plumb(vcs.Mem.RIOT, vcs.Mem.TIA)
vcs.TIA.Plumb(vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
// reset peripherals after new state has been plumbed. without this,
// controllers can feel odd if the newly plumbed state has left RIOT memory
// in a latched state
vcs.RIOT.Ports.ResetPeripherals()
vcs.Input.Plumb(vcs.RIOT.Ports)
}
// AttachCartridge to this VCS. While this function can be called directly it
// is advised that the setup package be used in most circumstances.
func (vcs *VCS) AttachCartridge(cartload cartridgeloader.Loader) error {
@ -226,29 +245,7 @@ func (vcs *VCS) SetClockSpeed(tvSpec string) error {
// recorders and playback, and RIOT plug monitor.
func (vcs *VCS) DetatchEmulationExtras() {
vcs.TIA.Audio.SetTracker(nil)
vcs.RIOT.Ports.AttachEventRecorder(nil)
vcs.RIOT.Ports.AttachPlayback(nil)
vcs.Input.AttachRecorder(nil)
vcs.Input.AttachPlayback(nil)
vcs.RIOT.Ports.AttachPlugMonitor(nil)
}
// HandleInputEvent forwards an input event to VCS Ports.
//
// It's important that this function be used in preference to calling the
// HandleInputEvent() function in the RIOT.Ports directly. For rewind reasons
// it is likely that any direct reference to RIOT.Ports will grow stale.
func (vcs *VCS) HandleInputEvent(inp ports.InputEvent) (bool, error) {
return vcs.RIOT.Ports.HandleInputEvent(inp)
}
// PeripheralID forwards a request of the PeripheralID of the PortID to VCS
// Ports.
//
// See important comment in HandleInputEvent() above.
func (vcs *VCS) PeripheralID(id plugging.PortID) plugging.PeripheralID {
return vcs.RIOT.Ports.PeripheralID(id)
}
// QueueEvent forwards an input event to VCS Ports.
func (vcs *VCS) QueueEvent(inp ports.InputEvent) error {
return vcs.RIOT.Ports.QueueEvent(inp)
}

View file

@ -13,12 +13,10 @@
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
// Package recorder handles recording and playback of user input. The Recorder
// type implements the riot.input.EventRecorder() interface. Once added as a
// transcriber to the VCS port, it will record all user input to the specified
// file.
// Package recorder handles recording and playback of input through the VCS
// input system. See input package.
//
// To keep things simple, recording gameplay will use the VCS in it's default
// state. Future versions of the recorder fileformat will support localised
// preferences.
// To keep things simple, recording gameplay will use the VCS in it's
// normalised state. Future versions of the recorder fileformat will support
// localised preferences.
package recorder

View file

@ -74,6 +74,9 @@ func (plb Playback) EndFrame() (bool, error) {
}
// NewPlayback is the preferred method of implementation for the Playback type.
//
// The returned playback must be attached to the VCS input system (with
// AttachToVCSInput() function) for it it to be useful.
func NewPlayback(transcript string) (*Playback, error) {
var err error
@ -172,11 +175,11 @@ func NewPlayback(transcript string) (*Playback, error) {
return plb, nil
}
// AttachToVCS attaches the playback instance (an implementation of the
// playback interface) to all the ports of the VCS, including the panel.
// AttachToVCSInput attaches the playback instance (an implementation of the
// playback interface) to all the input system of the VCS.
//
// Note that this will reset the VCS.
func (plb *Playback) AttachToVCS(vcs *hardware.VCS) error {
// Note that the VCS instance will be normalised as a result of this call.
func (plb *Playback) AttachToVCSInput(vcs *hardware.VCS) error {
// check we're working with correct information
if vcs == nil || vcs.TV == nil {
return curated.Errorf("playback: no playback hardware available")
@ -201,11 +204,8 @@ func (plb *Playback) AttachToVCS(vcs *hardware.VCS) error {
return curated.Errorf("playback: %v", err)
}
// attach playback to all vcs ports
err = vcs.RIOT.Ports.AttachPlayback(plb)
if err != nil {
return curated.Errorf("playback: %v", err)
}
// attach playback to all VCS Input system
vcs.Input.AttachPlayback(plb)
return nil
}

View file

@ -44,7 +44,7 @@ type Recorder struct {
// type. Note that attaching of the Recorder to all the ports of the VCS
// (including the panel) is implicit in this function call.
//
// Note that this will reset the VCS.
// Note that the VCS instance will be normalised as a result of this call.
func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
var err error
@ -67,8 +67,8 @@ func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
return nil, curated.Errorf("recorder: %v", err)
}
// attach recorder to vcs peripherals, including the panel
vcs.RIOT.Ports.AttachEventRecorder(rec)
// attach recorder to vcs input system
vcs.Input.AttachRecorder(rec)
// video digester for playback verification
rec.digest, err = digest.NewVideo(vcs.TV)

View file

@ -132,7 +132,7 @@ func (reg *PlaybackRegression) regress(newRegression bool, output io.Writer, msg
// function according to the current features of the recorder package and
// the saved script
err = plb.AttachToVCS(vcs)
err = plb.AttachToVCSInput(vcs)
if err != nil {
return false, "", curated.Errorf("playback: %v", err)
}

View file

@ -378,15 +378,8 @@ func Plumb(vcs *hardware.VCS, state *State, fromDifferentEmulation bool) {
vcs.RIOT = state.RIOT.Snapshot()
vcs.TIA = state.TIA.Snapshot()
vcs.CPU.Plumb(vcs.Mem)
vcs.Mem.Plumb(fromDifferentEmulation)
vcs.RIOT.Plumb(vcs.Mem.RIOT, vcs.Mem.TIA)
vcs.TIA.Plumb(vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
// reset peripherals after new state has been plumbed. without this,
// controllers can feel odd if the newly plumbed state has left RIOT memory
// in a latched state
vcs.RIOT.Ports.ResetPeripherals()
// finish off plumbing process
vcs.Plumb(fromDifferentEmulation)
}
// run from the supplied state until the cooridinates are reached.

View file

@ -117,36 +117,36 @@ func (set PanelSetup) matchCartHash(hash string) bool {
func (set PanelSetup) apply(vcs *hardware.VCS) error {
if set.p0 {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: true}
if _, err := vcs.HandleInputEvent(inp); err != nil {
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
return err
}
} else {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: false}
if _, err := vcs.HandleInputEvent(inp); err != nil {
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
return err
}
}
if set.p1 {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: true}
if _, err := vcs.HandleInputEvent(inp); err != nil {
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
return err
}
} else {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: false}
if _, err := vcs.HandleInputEvent(inp); err != nil {
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
return err
}
}
if set.col {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: true}
if _, err := vcs.HandleInputEvent(inp); err != nil {
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
return err
}
} else {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: false}
if _, err := vcs.HandleInputEvent(inp); err != nil {
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
return err
}
}

View file

@ -22,12 +22,12 @@ import (
// HandleInput conceptualises data being sent to the console ports.
type HandleInput interface {
// PeripheralID identifies the device currently attached to the port.
PeripheralID(id plugging.PortID) plugging.PeripheralID
// HandleInputEvent forwards the Event and EventData to the device connected to the
// specified PortID.
//
// Returns true if the port understood and handled the event.
HandleInputEvent(ports.InputEvent) (bool, error)
// PeripheralID identifies the device currently attached to the port.
PeripheralID(id plugging.PortID) plugging.PeripheralID
}