From c5edff64e7d84cd48eade14b1c20dae8c8895af2 Mon Sep 17 00:00:00 2001 From: steve Date: Mon, 15 Apr 2019 06:45:17 +0100 Subject: [PATCH] o contollers - generalised controller support - opened the way for different stick implementations, including scripted playback --- debugger/commands.go | 10 +- debugger/debugger.go | 13 ++ errors/categories.go | 6 +- errors/messages.go | 6 +- hardware/peripherals/controller.go | 19 +++ hardware/peripherals/digitalsticks/splace.go | 101 ++++++++++++ hardware/peripherals/stick.go | 153 ++++++------------- hardware/vcs.go | 41 ++++- playmode/play.go | 11 ++ 9 files changed, 236 insertions(+), 124 deletions(-) create mode 100644 hardware/peripherals/controller.go create mode 100644 hardware/peripherals/digitalsticks/splace.go diff --git a/debugger/commands.go b/debugger/commands.go index 14b82b6d..f72d55fe 100644 --- a/debugger/commands.go +++ b/debugger/commands.go @@ -878,12 +878,20 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens) (parseCommandResul } case cmdStick: + var err error + stick, _ := tokens.Get() action, _ := tokens.Get() stickN, _ := strconv.Atoi(stick) - err := dbg.vcs.Controller.HandleStick(stickN, action) + switch stickN { + case 0: + err = dbg.vcs.Player0.Handle(action) + case 1: + err = dbg.vcs.Player1.Handle(action) + } + if err != nil { return doNothing, err } diff --git a/debugger/debugger.go b/debugger/debugger.go index 6b0cbd0f..3a6d9fd7 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -11,6 +11,7 @@ import ( "gopher2600/hardware" "gopher2600/hardware/cpu/definitions" "gopher2600/hardware/cpu/result" + "gopher2600/hardware/peripherals/digitalsticks" "gopher2600/symbols" "os" "os/signal" @@ -137,6 +138,18 @@ func NewDebugger(tv gui.GUI) (*Debugger, error) { return nil, fmt.Errorf("error preparing VCS: %s", err) } + // create a controller + dst, err := digitalsticks.NewSplaceStick(0, dbg.vcs.Mem.TIA, dbg.vcs.Mem.RIOT, dbg.vcs.Panel) + if err != nil { + return nil, fmt.Errorf("error preparing VCS: %s", err) + } + + // attach a controller + err = dbg.vcs.AttachController(0, dst) + if err != nil { + return nil, fmt.Errorf("error preparing VCS: %s", err) + } + // create instance of disassembly -- the same base structure is used // for disassemblies subseuquent to the first one. dbg.disasm = &disassembly.Disassembly{} diff --git a/errors/categories.go b/errors/categories.go index 99275636..a748fab7 100644 --- a/errors/categories.go +++ b/errors/categories.go @@ -52,13 +52,11 @@ const ( ImageTV DigestTV - // Controllers - StickDisconnected - // GUI UnknownGUIRequest SDL // Peripherals - NoControllersFound + NoControllerHardware + NoPlayerPort ) diff --git a/errors/messages.go b/errors/messages.go index 602f9901..a271643c 100644 --- a/errors/messages.go +++ b/errors/messages.go @@ -49,13 +49,11 @@ var messages = map[Errno]string{ ImageTV: "ImageTV: %s", DigestTV: "DigestTV: %s", - // Controllers - StickDisconnected: "Stick for player %d is not connected", - // GUI UnknownGUIRequest: "GUI does not support %v request", SDL: "SDL: %s", // Peripherals - NoControllersFound: "no controllers found", + NoControllerHardware: "no hardware controllers found", + NoPlayerPort: "VCS only supports two players (0 and 1)", } diff --git a/hardware/peripherals/controller.go b/hardware/peripherals/controller.go new file mode 100644 index 00000000..fe8b631a --- /dev/null +++ b/hardware/peripherals/controller.go @@ -0,0 +1,19 @@ +package peripherals + +// Controller defines the operations required for VCS controllers +type Controller interface { + // make sure the most recent input is ready for the emulation + Strobe() error + + // handle interprets the supplied action and updates the emulation + Handle(action string) error + + // add InputTranscriber implementation for consideration by the controller + RegisterTranscriber(Transcriber) +} + +// Transcriber defines the operation required for a transcriber (observer) of +// VCS controller input +type Transcriber interface { + Transcribe(action string) +} diff --git a/hardware/peripherals/digitalsticks/splace.go b/hardware/peripherals/digitalsticks/splace.go new file mode 100644 index 00000000..3b2ae36b --- /dev/null +++ b/hardware/peripherals/digitalsticks/splace.go @@ -0,0 +1,101 @@ +package digitalsticks + +import ( + "gopher2600/errors" + "gopher2600/hardware/memory" + "gopher2600/hardware/peripherals" + + "github.com/splace/joysticks" +) + +// SplaceStick emulaes the digital VCS joystick +type SplaceStick struct { + *peripherals.DigitalStick + + device *joysticks.HID + err error +} + +// NewSplaceStick is the preferred method of initialisation for the Stick type +func NewSplaceStick(player int, tia memory.PeriphBus, riot memory.PeriphBus, panel *peripherals.Panel) (*SplaceStick, error) { + var err error + + sps := new(SplaceStick) + sps.DigitalStick, err = peripherals.NewDigitalStick(player, riot, tia) + if err != nil { + return nil, err + } + + // there is a flaw (either in splace/joysticks or somewehere else lower + // down in the kernel driver) which means that Connect() will not return + // until it recieves some input from the controller. to get around this, + // we've put the main body of the NewStick() function in a go routine. + go func() { + // try connecting to specific controller. + // system assigned index: typically increments on each new controller added. + sps.device = joysticks.Connect(1) + if sps.device == nil { + sps.err = errors.NewFormattedError(errors.NoControllerHardware, nil) + return + } + + // get/assign channels for specific events + stickMove := sps.device.OnMove(1) + + buttonPress := sps.device.OnClose(1) + buttonRelease := sps.device.OnOpen(1) + + // on xbox controller, button 8 is the start button + resetPress := sps.device.OnClose(8) + resetRelease := sps.device.OnOpen(8) + + // on xbox controller, button 9 is the back button + selectPress := sps.device.OnClose(7) + selectRelease := sps.device.OnOpen(7) + + // start feeding OS events onto the event channels. + go sps.device.ParcelOutEvents() + + // handle event channels + for { + select { + case <-resetPress: + panel.SetGameReset(true) + case <-resetRelease: + panel.SetGameReset(false) + + case <-selectPress: + panel.SetGameSelect(true) + case <-selectRelease: + panel.SetGameSelect(false) + + case <-buttonPress: + sps.DigitalStick.Handle("FIRE") + case <-buttonRelease: + sps.DigitalStick.Handle("NOFIRE") + + case ev := <-stickMove: + x := ev.(joysticks.CoordsEvent).X + y := ev.(joysticks.CoordsEvent).Y + if x < -0.5 { + sps.DigitalStick.Handle("LEFT") + } else if x > 0.5 { + sps.DigitalStick.Handle("RIGHT") + } else if y < -0.5 { + sps.DigitalStick.Handle("UP") + } else if y > 0.5 { + sps.DigitalStick.Handle("DOWN") + } else { + sps.DigitalStick.Handle("CENTRE") + } + } + } + }() + + return sps, nil +} + +// Strobe implements the Controller interface +func (sps *SplaceStick) Strobe() error { + return nil +} diff --git a/hardware/peripherals/stick.go b/hardware/peripherals/stick.go index 5ab527ae..45873de4 100644 --- a/hardware/peripherals/stick.go +++ b/hardware/peripherals/stick.go @@ -1,138 +1,77 @@ package peripherals import ( - "fmt" "gopher2600/errors" "gopher2600/hardware/memory" "gopher2600/hardware/memory/vcssymbols" "strings" - - "github.com/splace/joysticks" ) -// Stick emulaes the digital VCS joystick -type Stick struct { - device *joysticks.HID - err error - - tia memory.PeriphBus - riot memory.PeriphBus +// DigitalStick is the minimal implementation for the VCS joystick +type DigitalStick struct { + riot memory.PeriphBus + tia memory.PeriphBus + stickAddress uint16 + fireAddress uint16 + transcriber Transcriber } -// NewStick is the preferred method of initialisation for the Stick type -func NewStick(tia memory.PeriphBus, riot memory.PeriphBus, panel *Panel) *Stick { - stk := new(Stick) - stk.tia = tia - stk.riot = riot +// NewDigitalStick is the preferred method of initialisation the DigitalStick +// type +func NewDigitalStick(player int, riot memory.PeriphBus, tia memory.PeriphBus) (*DigitalStick, error) { + dst := &DigitalStick{riot: riot, tia: tia} // TODO: make all this work with a second contoller. for now, initialise // and asssume that there is just one controller for player 0 - stk.riot.PeriphWrite(vcssymbols.SWCHA, 0xff) - stk.tia.PeriphWrite(vcssymbols.INPT4, 0x80) - stk.tia.PeriphWrite(vcssymbols.INPT5, 0x80) - - // there is a flaw (either in splace/joysticks or somewehere else lower - // down in the kernel driver) which means that Connect() will not return - // until it recieves some input from the controller. to get around this, - // we've put the main body of the NewStick() function in a go routine. - go func() { - // try connecting to specific controller. - // system assigned index: typically increments on each new controller added. - stk.device = joysticks.Connect(1) - if stk.device == nil { - stk.err = errors.NewFormattedError(errors.NoControllersFound, nil) - return - } - - // get/assign channels for specific events - stickMove := stk.device.OnMove(1) - - buttonPress := stk.device.OnClose(1) - buttonRelease := stk.device.OnOpen(1) - - // on xbox controller, button 8 is the start button - resetPress := stk.device.OnClose(8) - resetRelease := stk.device.OnOpen(8) - - // on xbox controller, button 9 is the back button - selectPress := stk.device.OnClose(7) - selectRelease := stk.device.OnOpen(7) - - // start feeding OS events onto the event channels. - go stk.device.ParcelOutEvents() - - // handle event channels - for { - select { - case <-resetPress: - panel.SetGameReset(true) - case <-resetRelease: - panel.SetGameReset(false) - - case <-selectPress: - panel.SetGameSelect(true) - case <-selectRelease: - panel.SetGameSelect(false) - - case <-buttonPress: - stk.HandleStick(0, "FIRE") - case <-buttonRelease: - stk.HandleStick(0, "NOFIRE") - - case ev := <-stickMove: - x := ev.(joysticks.CoordsEvent).X - y := ev.(joysticks.CoordsEvent).Y - if x < -0.5 { - stk.HandleStick(0, "LEFT") - } else if x > 0.5 { - stk.HandleStick(0, "RIGHT") - } else if y < -0.5 { - stk.HandleStick(0, "UP") - } else if y > 0.5 { - stk.HandleStick(0, "DOWN") - } else { - stk.HandleStick(0, "CENTRE") - } - } - } - }() - - return stk -} - -// HandleStick parses the action and writes to the correct memory location -func (stk *Stick) HandleStick(player int, action string) error { - var stickAddress uint16 - var fireAddress uint16 + dst.riot.PeriphWrite(vcssymbols.SWCHA, 0xff) + dst.tia.PeriphWrite(vcssymbols.INPT4, 0x80) + dst.tia.PeriphWrite(vcssymbols.INPT5, 0x80) if player == 0 { - stickAddress = vcssymbols.SWCHA - fireAddress = vcssymbols.INPT4 + dst.stickAddress = vcssymbols.SWCHA + dst.fireAddress = vcssymbols.INPT4 } else if player == 1 { - stickAddress = vcssymbols.SWCHB - fireAddress = vcssymbols.INPT5 + dst.stickAddress = vcssymbols.SWCHB + dst.fireAddress = vcssymbols.INPT5 } else { - panic(fmt.Sprintf("there is no player %d with a joystick to handle", player)) + return nil, errors.NewFormattedError(errors.NoPlayerPort) } + return dst, nil +} + +// Handle implements the Controller interface +func (dst DigitalStick) Handle(action string) error { switch strings.ToUpper(action) { case "LEFT": - stk.riot.PeriphWrite(stickAddress, 0xbf) + dst.riot.PeriphWrite(dst.stickAddress, 0xbf) case "RIGHT": - stk.riot.PeriphWrite(stickAddress, 0x7f) + dst.riot.PeriphWrite(dst.stickAddress, 0x7f) case "UP": - stk.riot.PeriphWrite(stickAddress, 0xef) + dst.riot.PeriphWrite(dst.stickAddress, 0xef) case "DOWN": - stk.riot.PeriphWrite(stickAddress, 0xdf) - case "CENTER": - fallthrough - case "CENTRE": - stk.riot.PeriphWrite(stickAddress, 0xff) + dst.riot.PeriphWrite(dst.stickAddress, 0xdf) + case "CENTRE", "CENTER": + dst.riot.PeriphWrite(dst.stickAddress, 0xff) case "FIRE": - stk.tia.PeriphWrite(fireAddress, 0x00) + dst.tia.PeriphWrite(dst.fireAddress, 0x00) case "NOFIRE": - stk.tia.PeriphWrite(fireAddress, 0x80) + dst.tia.PeriphWrite(dst.fireAddress, 0x80) + } + + if dst.transcriber != nil { + dst.transcriber.Transcribe(action) } return nil } + +// RegisterTranscriber implements the Controller interface +func (dst *DigitalStick) RegisterTranscriber(trans Transcriber) { + dst.transcriber = trans +} + +// Strobe implements the Controller interface +func (dst *DigitalStick) Strobe() error { + return nil +} diff --git a/hardware/vcs.go b/hardware/vcs.go index 4d3ae79e..eae308e0 100644 --- a/hardware/vcs.go +++ b/hardware/vcs.go @@ -2,6 +2,7 @@ package hardware import ( "fmt" + "gopher2600/errors" "gopher2600/hardware/cpu" "gopher2600/hardware/cpu/result" "gopher2600/hardware/memory" @@ -22,8 +23,10 @@ type VCS struct { // tv is not part of the VCS but is attached to it TV television.Television - Panel *peripherals.Panel - Controller *peripherals.Stick + Panel *peripherals.Panel + + Player0 peripherals.Controller + Player1 peripherals.Controller } // NewVCS creates a new VCS and everything associated with the hardware. It is @@ -59,15 +62,23 @@ func NewVCS(tv television.Television) (*VCS, error) { return nil, fmt.Errorf("can't create console control panel") } - // TODO: better contoller support - vcs.Controller = peripherals.NewStick(vcs.Mem.TIA, vcs.Mem.RIOT, vcs.Panel) - if vcs.Controller == nil { - return nil, fmt.Errorf("can't create stick controller") - } - return vcs, nil } +// AttachController allows control of the emulation with the Contoller +// implementation +func (vcs *VCS) AttachController(player int, controller peripherals.Controller) error { + switch player { + case 0: + vcs.Player0 = controller + case 1: + vcs.Player1 = controller + default: + return errors.NewFormattedError(errors.NoPlayerPort) + } + return nil +} + // AttachCartridge loads a cartridge (given by filename) into the emulators memory func (vcs *VCS) AttachCartridge(filename string) error { if filename == "" { @@ -116,6 +127,15 @@ func (vcs *VCS) Reset() error { return nil } +func (vcs *VCS) strobeControllers() { + if vcs.Player0 != nil { + vcs.Player0.Strobe() + } + if vcs.Player1 != nil { + vcs.Player1.Strobe() + } +} + // Step the emulator state one CPU instruction // -- we can put this function in a loop for an effective debugging loop // ths videoCycleCallback function for an additional callback point in the @@ -135,6 +155,9 @@ func (vcs *VCS) Step(videoCycleCallback func(*result.Instruction) error) (int, * cycleVCS := func(r *result.Instruction) { cpuCycles++ + // ensure controllers have updated their input + vcs.strobeControllers() + // run riot only once per CPU cycle // TODO: not sure when in the video cycle sequence it should be run // TODO: is this something that can drift, thereby causing subtly different @@ -208,6 +231,7 @@ func (vcs *VCS) RunConcurrent(running *atomic.Value) error { }() cycleVCS := func(r *result.Instruction) { + vcs.strobeControllers() triggerTIA <- true triggerRIOT <- true <-triggerTIA @@ -231,6 +255,7 @@ func (vcs *VCS) Run(continueCheck func() bool) error { var err error cycleVCS := func(r *result.Instruction) { + vcs.strobeControllers() vcs.RIOT.ReadRIOTMemory() vcs.RIOT.Step() vcs.TIA.ReadTIAMemory() diff --git a/playmode/play.go b/playmode/play.go index f34df321..263cc55a 100644 --- a/playmode/play.go +++ b/playmode/play.go @@ -5,6 +5,7 @@ import ( "gopher2600/gui" "gopher2600/gui/sdl" "gopher2600/hardware" + "gopher2600/hardware/peripherals/digitalsticks" "sync/atomic" ) @@ -20,6 +21,16 @@ func Play(cartridgeFile, tvMode string, scaling float32, stable bool) error { return fmt.Errorf("error preparing VCS: %s", err) } + dst, err := digitalsticks.NewSplaceStick(0, vcs.Mem.TIA, vcs.Mem.RIOT, vcs.Panel) + if err != nil { + return err + } + + err = vcs.AttachController(0, dst) + if err != nil { + return err + } + err = vcs.AttachCartridge(cartridgeFile) if err != nil { return err