o contollers

- generalised controller support
    - opened the way for different stick implementations, including
    scripted playback
This commit is contained in:
steve 2019-04-15 06:45:17 +01:00
parent b03b64e527
commit c5edff64e7
9 changed files with 236 additions and 124 deletions

View file

@ -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
}

View file

@ -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{}

View file

@ -52,13 +52,11 @@ const (
ImageTV
DigestTV
// Controllers
StickDisconnected
// GUI
UnknownGUIRequest
SDL
// Peripherals
NoControllersFound
NoControllerHardware
NoPlayerPort
)

View file

@ -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)",
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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()

View file

@ -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