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) AddAudioMixer(television.AudioMixer)
} }
// VCS defines the VCS functions required by a bot. // Input defines the Input functions required by a bot.
type VCS interface { type Input interface {
QueueEvent(ports.InputEvent) error PushEvent(ports.InputEvent) error
} }
// Diagnostic instances are sent over the Feedback Diagnostic channel. // Diagnostic instances are sent over the Feedback Diagnostic channel.

View file

@ -152,8 +152,8 @@ func (obs *observer) EndRendering() error {
type videoChessBot struct { type videoChessBot struct {
obs *observer obs *observer
vcs bots.VCS input bots.Input
tv bots.TV tv bots.TV
// quit as soon as possible when a value appears on the channel // quit as soon as possible when a value appears on the channel
quit chan bool quit chan bool
@ -364,9 +364,9 @@ func (bot *videoChessBot) moveCursorOnceStep(portid plugging.PortID, direction p
waiting := true waiting := true
for waiting { 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.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 { select {
case <-bot.obs.audioFeedback: case <-bot.obs.audioFeedback:
waiting = false waiting = false
@ -462,9 +462,9 @@ func (bot *videoChessBot) moveCursor(moveCol int, moveRow int, shortcut bool) {
waiting := true waiting := true
for waiting { 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.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 { select {
case <-bot.obs.audioFeedback: case <-bot.obs.audioFeedback:
waiting = false 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). // 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{ bot := &videoChessBot{
obs: newObserver(), obs: newObserver(),
vcs: vcs, input: vcs,
tv: tv, tv: tv,
quit: make(chan bool), quit: make(chan bool),
prevPosition: image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines)), 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. // Bots keeps track of the running bot and handles loading and termination.
type Bots struct { type Bots struct {
vcs bots.VCS input bots.Input
tv bots.TV tv bots.TV
running bots.Bot running bots.Bot
} }
// NewBots is the preferred method of initialisation for the Bots type. // 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{ return &Bots{
vcs: vcs, input: input,
tv: tv, tv: tv,
} }
} }
@ -48,7 +48,7 @@ func (b *Bots) ActivateBot(cartHash string) (*bots.Feedback, error) {
switch cartHash { switch cartHash {
case "043ef523e4fcb9fc2fc2fda21f15671bf8620fc3": case "043ef523e4fcb9fc2fc2fda21f15671bf8620fc3":
b.running, err = chess.NewVideoChess(b.vcs, b.tv) b.running, err = chess.NewVideoChess(b.input, b.tv)
if err != nil { if err != nil {
return nil, curated.Errorf("bots: %v", err) return nil, curated.Errorf("bots: %v", err)
} }

View file

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

View file

@ -1352,55 +1352,55 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
switch strings.ToUpper(arg) { switch strings.ToUpper(arg) {
case "P0": case "P0":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer0Pro, D: nil} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer0Pro, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1": case "P1":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer1Pro, D: nil} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer1Pro, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "COL": case "COL":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelToggleColor, D: nil} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelToggleColor, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
} }
case "SET": case "SET":
arg, _ := tokens.Get() arg, _ := tokens.Get()
switch strings.ToUpper(arg) { switch strings.ToUpper(arg) {
case "P0PRO": case "P0PRO":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: true} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: true}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1PRO": case "P1PRO":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: true} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: true}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P0AM": case "P0AM":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: false} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: false}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1AM": case "P1AM":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: false} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: false}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "COL": case "COL":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: true} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: true}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "BW": case "BW":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: false} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: false}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
} }
case "HOLD": case "HOLD":
arg, _ := tokens.Get() arg, _ := tokens.Get()
switch strings.ToUpper(arg) { switch strings.ToUpper(arg) {
case "SELECT": case "SELECT":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: true} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: true}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "RESET": case "RESET":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: true} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: true}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
} }
case "RELEASE": case "RELEASE":
arg, _ := tokens.Get() arg, _ := tokens.Get()
switch strings.ToUpper(arg) { switch strings.ToUpper(arg) {
case "SELECT": case "SELECT":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: false} inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: false}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case "RESET": case "RESET":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: false} 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 { switch n {
case 0: case 0:
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: event, D: value} inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: event, D: value}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
case 1: case 1:
inp := ports.InputEvent{Port: plugging.PortRightPlayer, Ev: event, D: value} inp := ports.InputEvent{Port: plugging.PortRightPlayer, Ev: event, D: value}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
} }
if err != nil { if err != nil {
@ -1478,18 +1478,18 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
case 0: case 0:
if strings.ToUpper(key) == "NONE" { if strings.ToUpper(key) == "NONE" {
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadUp, D: nil} inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadUp, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
} else { } else {
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadDown, D: rune(key[0])} 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: case 1:
if strings.ToUpper(key) == "NONE" { if strings.ToUpper(key) == "NONE" {
inp := ports.InputEvent{Port: plugging.PortRightPlayer, Ev: ports.KeypadUp, D: nil} inp := ports.InputEvent{Port: plugging.PortRightPlayer, Ev: ports.KeypadUp, D: nil}
_, err = dbg.vcs.HandleInputEvent(inp) _, err = dbg.vcs.Input.HandleInputEvent(inp)
} else { } else {
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadDown, D: rune(key[0])} 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/hardware/television/coords"
"github.com/jetsetilly/gopher2600/logger" "github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/prefs" "github.com/jetsetilly/gopher2600/prefs"
"github.com/jetsetilly/gopher2600/recorder"
"github.com/jetsetilly/gopher2600/reflection" "github.com/jetsetilly/gopher2600/reflection"
"github.com/jetsetilly/gopher2600/reflection/counter" "github.com/jetsetilly/gopher2600/reflection/counter"
"github.com/jetsetilly/gopher2600/resources/unique"
"github.com/jetsetilly/gopher2600/rewind" "github.com/jetsetilly/gopher2600/rewind"
"github.com/jetsetilly/gopher2600/setup" "github.com/jetsetilly/gopher2600/setup"
"github.com/jetsetilly/gopher2600/tracker" "github.com/jetsetilly/gopher2600/tracker"
@ -85,6 +87,10 @@ type Debugger struct {
// sure Close() is called on end // sure Close() is called on end
loader *cartridgeloader.Loader loader *cartridgeloader.Loader
// gameplay recorder/playback
recorder *recorder.Recorder
playback *recorder.Playback
// comparison emulator // comparison emulator
comparison *comparison.Comparison comparison *comparison.Comparison
@ -304,7 +310,7 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
// create userinput/controllers handler // create userinput/controllers handler
dbg.controllers = userinput.NewControllers() dbg.controllers = userinput.NewControllers()
dbg.controllers.AddInputHandler(dbg.vcs) dbg.controllers.AddInputHandler(dbg.vcs.Input)
// replace player 1 port with savekey // replace player 1 port with savekey
if useSavekey { if useSavekey {
@ -315,7 +321,7 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
} }
// create bot coordinator // 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 // set up debugging interface to memory
dbg.dbgmem = &dbgmem.DbgMem{ dbg.dbgmem = &dbgmem.DbgMem{
@ -400,6 +406,12 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
dbg.vcs.TV.SetFPSCap(fpsCap) dbg.vcs.TV.SetFPSCap(fpsCap)
dbg.gui.SetFeature(gui.ReqMonitorSync, 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 return dbg, nil
} }
@ -449,10 +461,13 @@ func (dbg *Debugger) setState(state emulation.State) {
// * see setState() comment, although debugger.SetFeature(ReqSetPause) will // * see setState() comment, although debugger.SetFeature(ReqSetPause) will
// always be "noisy" // always be "noisy"
func (dbg *Debugger) setStateQuiet(state emulation.State, quiet bool) { 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 { if state == emulation.Rewinding {
dbg.endPlayback()
dbg.endRecording()
dbg.endComparison() 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) dbg.vcs.TV.SetEmulationState(state)
@ -487,6 +502,9 @@ func (dbg *Debugger) setMode(mode emulation.Mode) error {
return nil 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 // if there is a halting condition that is not allowed in playmode (see
// targets type) then do not change the emulation mode // 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. // End cleans up any resources that may be dangling.
func (dbg *Debugger) end() { func (dbg *Debugger) end() {
dbg.endPlayback()
dbg.endRecording()
dbg.endComparison() dbg.endComparison()
dbg.bots.Quit() dbg.bots.Quit()
dbg.vcs.End() dbg.vcs.End()
defer dbg.term.CleanUp()
// set ending state // set ending state
err := dbg.gui.SetFeature(gui.ReqEnd) err := dbg.gui.SetFeature(gui.ReqEnd)
if err != nil { if err != nil {
@ -582,57 +605,33 @@ func (dbg *Debugger) end() {
} }
} }
// Starts the main emulation sequence. // StartInDebugMode starts the emulation with the debugger activated.
func (dbg *Debugger) Start(mode emulation.Mode, initScript string, cartload cartridgeloader.Loader, comparisonROM string, comparisonPrefs string) error { func (dbg *Debugger) StartInDebugMode(initScript string, filename string, mapping string) error {
// do not allow comparison emulation inside the debugger. it's far too // set running flag as early as possible
// complicated running two emulations that must be synced in the debugger dbg.running = true
// loop
if mode == emulation.ModeDebugger && comparisonROM != "" {
return curated.Errorf("debugger: cannot run comparison emulation inside the debugger")
}
var err error var err error
var cartload cartridgeloader.Loader
defer dbg.end() if filename == "" {
err = dbg.start(mode, initScript, cartload, comparisonROM, comparisonPrefs) cartload = cartridgeloader.Loader{}
if err != nil { } else {
if curated.Has(err, terminal.UserQuit) { cartload, err = cartridgeloader.NewLoader(filename, mapping)
return nil 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) err = dbg.attachCartridge(cartload)
if err != nil { if err != nil {
return curated.Errorf("debugger: %v", err) return curated.Errorf("debugger: %v", err)
} }
err = dbg.startComparison(comparisonROM, comparisonPrefs) err = dbg.setMode(emulation.ModeDebugger)
if err != nil { if err != nil {
return curated.Errorf("debugger: %v", err) 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 != "" { if initScript != "" {
scr, err := script.RescribeScript(initScript) scr, err := script.RescribeScript(initScript)
if err == nil { 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 // end script recording gracefully. this way we don't have to worry too
// hard about script scribes // hard about script scribes
defer func() { defer func() {
@ -660,7 +733,7 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
for dbg.running { for dbg.running {
switch dbg.Mode() { switch dbg.Mode() {
case emulation.ModePlay: case emulation.ModePlay:
err = dbg.playLoop() err := dbg.playLoop()
if err != nil { if err != nil {
// if we ever encounter a cartridge ejected error in playmode // if we ever encounter a cartridge ejected error in playmode
// then simply open up the ROM selector // 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()) 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 { if err != nil {
return curated.Errorf("debugger: %v", err) 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 // make sure any cartridge loader has been finished with
if dbg.loader != nil { if dbg.loader != nil {
err = dbg.loader.Close() err := dbg.loader.Close()
if err != nil { if err != nil {
return curated.Errorf("debugger: %v", err) 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. // 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. // especially important is the repointing of the symbols table in the instance of dbgmem.
func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error) { 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.endComparison()
dbg.bots.Quit() dbg.bots.Quit()
@ -913,6 +988,63 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
return nil 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 { func (dbg *Debugger) startComparison(comparisonROM string, comparisonPrefs string) error {
if comparisonROM == "" { if comparisonROM == "" {
return nil return nil

View file

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

View file

@ -44,8 +44,6 @@ import (
"github.com/jetsetilly/gopher2600/statsview" "github.com/jetsetilly/gopher2600/statsview"
) )
const defaultInitScript = "debuggerInit"
// communication between the main goroutine and the launch goroutine. // communication between the main goroutine and the launch goroutine.
type mainSync struct { type mainSync struct {
state chan stateRequest state chan stateRequest
@ -267,145 +265,7 @@ func launch(sync *mainSync) {
sync.state <- stateRequest{req: reqQuit} sync.state <- stateRequest{req: reqQuit}
} }
// func play(md *modalflag.Modes, sync *mainSync) error { const defaultInitScript = "debuggerInit"
// 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
// }
func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync) error { func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync) error {
md.NewMode() 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") mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL, PAL60") spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL, PAL60")
fpsCap := md.AddBool("fpscap", true, "cap fps to TV specification") 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") 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") 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") 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 comparisonROM *string
var comparisonPrefs *string var comparisonPrefs *string
var record *bool
if emulationMode == emulation.ModePlay { if emulationMode == emulation.ModePlay {
comparisonROM = md.AddString("comparisonROM", "", "ROM to run in parallel for comparison") comparisonROM = md.AddString("comparisonROM", "", "ROM to run in parallel for comparison")
comparisonPrefs = md.AddString("comparisonPrefs", "", "preferences for comparison emulation") 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() { if statsview.Available() {
stats = md.AddBool("statsview", false, fmt.Sprintf("run stats server (%s)", statsview.Address)) stats = md.AddBool("statsview", false, fmt.Sprintf("run stats server (%s)", statsview.Address))
} }
// parse arguments
p, err := md.Parse() p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue { if err != nil || p != modalflag.ParseContinue {
return err return err
} }
// check remaining arguments
if len(md.RemainingArgs()) > 1 {
return fmt.Errorf("too many arguments for %s mode", md)
}
// set debugging log echo // set debugging log echo
if *log { if *log {
logger.SetEcho(os.Stdout) logger.SetEcho(os.Stdout)
@ -449,7 +328,7 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
logger.SetEcho(nil) logger.SetEcho(nil)
} }
if *stats { if stats != nil && *stats {
statsview.Launch(os.Stdout) 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 // turning the emulation's interrupt handler off
sync.state <- stateRequest{req: reqNoIntSig} sync.state <- stateRequest{req: reqNoIntSig}
// GUI create function
create := func(e emulation.Emulation) (gui.GUI, terminal.Terminal, error) { create := func(e emulation.Emulation) (gui.GUI, terminal.Terminal, error) {
var term terminal.Terminal var term terminal.Terminal
var scr gui.GUI var scr gui.GUI
@ -512,23 +392,6 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
return err 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 // check for profiling options
prf, err := performance.ParseProfileString(*profile) prf, err := performance.ParseProfileString(*profile)
if err != nil { if err != nil {
@ -537,23 +400,21 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
// set up a launch function // set up a launch function
dbgLaunch := func() error { dbgLaunch := func() error {
// check if comparison was defined and dereference if it was, otherwise
// comp is just the empty string switch emulationMode {
var comp string case emulation.ModeDebugger:
if comparisonROM != nil { err := dbg.StartInDebugMode(*initScript, md.GetArg(0), *mapping)
comp = *comparisonROM 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 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 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) { func (mem *Memory) Plumb(fromDifferentEmulation bool) {
mem.Cart.Plumb(fromDifferentEmulation) mem.Cart.Plumb(fromDifferentEmulation)
} }

View file

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

View file

@ -117,23 +117,3 @@ type TimedInputEvent struct {
Time coords.TelevisionCoords Time coords.TelevisionCoords
InputEvent 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/addresses"
"github.com/jetsetilly/gopher2600/hardware/memory/bus" "github.com/jetsetilly/gopher2600/hardware/memory/bus"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging" "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). // Input implements the input/output part of the RIOT (the IO in RIOT).
type Ports struct { type Ports struct {
riot bus.ChipBus riot bus.ChipBus
@ -40,9 +34,6 @@ type Ports struct {
LeftPlayer Peripheral LeftPlayer Peripheral
RightPlayer Peripheral RightPlayer Peripheral
playback EventPlayback
recorder []EventRecorder
monitor plugging.PlugMonitor monitor plugging.PlugMonitor
// local copies of key chip memory registers // 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 // we use it to mux the Player0 and Player 1 nibbles into the single register
swchaMux uint8 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. // 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{ p := &Ports{
riot: riotMem, riot: riotMem,
tia: tiaMem, tia: tiaMem,
recorder: make([]EventRecorder, 0),
swchaFromCPU: 0x00, swchaFromCPU: 0x00,
swacnt: 0x00, swacnt: 0x00,
latch: false, latch: false,
pushed: make(chan InputEvent, 64),
} }
return p return p
} }
@ -267,45 +241,6 @@ func (p *Ports) Step() {
p.Panel.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. // AttchPlugMonitor implements the plugging.Monitorable interface.
func (p *Ports) AttachPlugMonitor(m plugging.PlugMonitor) { func (p *Ports) AttachPlugMonitor(m plugging.PlugMonitor) {
p.monitor = m p.monitor = m
@ -328,10 +263,7 @@ func (p *Ports) AttachPlugMonitor(m plugging.PlugMonitor) {
} }
} }
// PeripheralID implements userinput.HandleInput interface. // PeripheralID returns the ID of the peripheral in the identified port.
//
// Consider using PeripheralID() function in the VCS type rather than this
// function directly.
func (p *Ports) PeripheralID(id plugging.PortID) plugging.PeripheralID { func (p *Ports) PeripheralID(id plugging.PortID) plugging.PeripheralID {
switch id { switch id {
case plugging.PortPanel: case plugging.PortPanel:
@ -376,3 +308,26 @@ func (p *Ports) WriteINPTx(inptx addresses.ChipRegister, data uint8) {
p.tia.ChipWrite(inptx, data) 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 // see the equivalient videoCycle() in the VCS.Step() function for an
// explanation for what's going on here: // explanation for what's going on here:
videoCycle := func() error { videoCycle := func() error {
if err := vcs.RIOT.Ports.HandleInputEvents(); err != nil { if err := vcs.Input.Process(); err != nil {
return err 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 // I don't believe any visual or audible artefacts of the VCS (undocumented
// or not) rely on the details of the CPU-TIA relationship. // or not) rely on the details of the CPU-TIA relationship.
videoCycle := func() error { videoCycle := func() error {
if err := vcs.RIOT.Ports.HandleInputEvents(); err != nil { if err := vcs.Input.Process(); err != nil {
return err return err
} }

View file

@ -19,12 +19,12 @@ import (
"github.com/jetsetilly/gopher2600/cartridgeloader" "github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/curated" "github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/hardware/cpu" "github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/hardware/input"
"github.com/jetsetilly/gopher2600/hardware/instance" "github.com/jetsetilly/gopher2600/hardware/instance"
"github.com/jetsetilly/gopher2600/hardware/memory" "github.com/jetsetilly/gopher2600/hardware/memory"
"github.com/jetsetilly/gopher2600/hardware/memory/addresses" "github.com/jetsetilly/gopher2600/hardware/memory/addresses"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge" "github.com/jetsetilly/gopher2600/hardware/memory/cartridge"
"github.com/jetsetilly/gopher2600/hardware/riot" "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/controllers"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/panel" "github.com/jetsetilly/gopher2600/hardware/riot/ports/panel"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging" "github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
@ -50,6 +50,8 @@ type VCS struct {
RIOT *riot.RIOT RIOT *riot.RIOT
TIA *tia.TIA TIA *tia.TIA
Input *input.Input
// The Clock defines the basic speed at which the the machine is runningt. This governs // 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 // the speed of the CPU, the RIOT and attached peripherals. The TIA runs at
// exactly three times this speed. // 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.CPU = cpu.NewCPU(vcs.Instance, vcs.Mem)
vcs.RIOT = riot.NewRIOT(vcs.Instance, vcs.Mem.RIOT, vcs.Mem.TIA) 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) vcs.TIA, err = tia.NewTIA(vcs.Instance, vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
if err != nil { if err != nil {
return nil, err return nil, err
@ -120,6 +124,21 @@ func (vcs *VCS) End() {
vcs.TV.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 // AttachCartridge to this VCS. While this function can be called directly it
// is advised that the setup package be used in most circumstances. // is advised that the setup package be used in most circumstances.
func (vcs *VCS) AttachCartridge(cartload cartridgeloader.Loader) error { 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. // recorders and playback, and RIOT plug monitor.
func (vcs *VCS) DetatchEmulationExtras() { func (vcs *VCS) DetatchEmulationExtras() {
vcs.TIA.Audio.SetTracker(nil) vcs.TIA.Audio.SetTracker(nil)
vcs.RIOT.Ports.AttachEventRecorder(nil) vcs.Input.AttachRecorder(nil)
vcs.RIOT.Ports.AttachPlayback(nil) vcs.Input.AttachPlayback(nil)
vcs.RIOT.Ports.AttachPlugMonitor(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 // You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>. // along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
// Package recorder handles recording and playback of user input. The Recorder // Package recorder handles recording and playback of input through the VCS
// type implements the riot.input.EventRecorder() interface. Once added as a // input system. See input package.
// transcriber to the VCS port, it will record all user input to the specified
// file.
// //
// To keep things simple, recording gameplay will use the VCS in it's default // To keep things simple, recording gameplay will use the VCS in it's
// state. Future versions of the recorder fileformat will support localised // normalised state. Future versions of the recorder fileformat will support
// preferences. // localised preferences.
package recorder 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. // 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) { func NewPlayback(transcript string) (*Playback, error) {
var err error var err error
@ -172,11 +175,11 @@ func NewPlayback(transcript string) (*Playback, error) {
return plb, nil return plb, nil
} }
// AttachToVCS attaches the playback instance (an implementation of the // AttachToVCSInput attaches the playback instance (an implementation of the
// playback interface) to all the ports of the VCS, including the panel. // playback interface) to all the input system of the VCS.
// //
// Note that this will reset the VCS. // Note that the VCS instance will be normalised as a result of this call.
func (plb *Playback) AttachToVCS(vcs *hardware.VCS) error { func (plb *Playback) AttachToVCSInput(vcs *hardware.VCS) error {
// check we're working with correct information // check we're working with correct information
if vcs == nil || vcs.TV == nil { if vcs == nil || vcs.TV == nil {
return curated.Errorf("playback: no playback hardware available") 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) return curated.Errorf("playback: %v", err)
} }
// attach playback to all vcs ports // attach playback to all VCS Input system
err = vcs.RIOT.Ports.AttachPlayback(plb) vcs.Input.AttachPlayback(plb)
if err != nil {
return curated.Errorf("playback: %v", err)
}
return nil 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 // type. Note that attaching of the Recorder to all the ports of the VCS
// (including the panel) is implicit in this function call. // (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) { func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
var err error var err error
@ -67,8 +67,8 @@ func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
return nil, curated.Errorf("recorder: %v", err) return nil, curated.Errorf("recorder: %v", err)
} }
// attach recorder to vcs peripherals, including the panel // attach recorder to vcs input system
vcs.RIOT.Ports.AttachEventRecorder(rec) vcs.Input.AttachRecorder(rec)
// video digester for playback verification // video digester for playback verification
rec.digest, err = digest.NewVideo(vcs.TV) 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 // function according to the current features of the recorder package and
// the saved script // the saved script
err = plb.AttachToVCS(vcs) err = plb.AttachToVCSInput(vcs)
if err != nil { if err != nil {
return false, "", curated.Errorf("playback: %v", err) 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.RIOT = state.RIOT.Snapshot()
vcs.TIA = state.TIA.Snapshot() vcs.TIA = state.TIA.Snapshot()
vcs.CPU.Plumb(vcs.Mem) // finish off plumbing process
vcs.Mem.Plumb(fromDifferentEmulation) vcs.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()
} }
// run from the supplied state until the cooridinates are reached. // 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 { func (set PanelSetup) apply(vcs *hardware.VCS) error {
if set.p0 { if set.p0 {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: true} 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 return err
} }
} else { } else {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: false} 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 return err
} }
} }
if set.p1 { if set.p1 {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: true} 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 return err
} }
} else { } else {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: false} 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 return err
} }
} }
if set.col { if set.col {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: true} 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 return err
} }
} else { } else {
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: false} 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 return err
} }
} }

View file

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