Skip to content

Commit

Permalink
Add keyboard and mouse support
Browse files Browse the repository at this point in the history
  • Loading branch information
sergystepanov committed May 27, 2024
1 parent ca64bd1 commit 0d78ef7
Show file tree
Hide file tree
Showing 36 changed files with 1,339 additions and 460 deletions.
5 changes: 3 additions & 2 deletions pkg/api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ type (
PlayerIndex int `json:"player_index"`
}
GameStartUserResponse struct {
RoomId string `json:"roomId"`
Av *AppVideoInfo `json:"av"`
RoomId string `json:"roomId"`
Av *AppVideoInfo `json:"av"`
KbMouse bool `json:"kb_mouse"`
}
IceServer struct {
Urls string `json:"urls,omitempty"`
Expand Down
5 changes: 3 additions & 2 deletions pkg/api/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ type (
}
StartGameResponse struct {
Room
AV *AppVideoInfo `json:"av"`
Record bool
AV *AppVideoInfo `json:"av"`
Record bool `json:"record"`
KbMouse bool `json:"kb_mouse"`
}
RecordGameRequest[T Id] struct {
StatefulRoom[T]
Expand Down
8 changes: 4 additions & 4 deletions pkg/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,13 @@ emulator:
# - isGlAllowed (bool)
# - usesLibCo (bool)
# - hasMultitap (bool) -- (removed)
# - coreAspectRatio (bool) -- correct the aspect ratio on the client with the info from the core.
# - coreAspectRatio (bool) -- (deprecated) correct the aspect ratio on the client with the info from the core.
# - hid (map[int][]int)
# A list of device IDs to bind to the input ports.
# Can be seen in human readable form in the console when worker.debug is enabled.
# Some cores allow binding multiple devices to a single port (DosBox), but typically,
# you should bind just one device to one port.
# - kbMouseSupport (bool) -- (temp) a flag if the core needs the keyboard and mouse on the client
# - vfr (bool)
# (experimental)
# Enable variable frame rate only for cores that can't produce a constant frame rate.
Expand All @@ -213,7 +215,6 @@ emulator:
mgba_audio_low_pass_filter: enabled
mgba_audio_low_pass_range: 40
pcsx:
coreAspectRatio: true
lib: pcsx_rearmed_libretro
roms: [ "cue", "chd" ]
# example of folder override
Expand All @@ -227,7 +228,6 @@ emulator:
# https://docs.libretro.com/library/fbneo/
mame:
lib: fbneo_libretro
coreAspectRatio: true
roms: [ "zip" ]
nes:
lib: nestopia_libretro
Expand Down Expand Up @@ -280,7 +280,7 @@ encoder:
# see: https://trac.ffmpeg.org/wiki/Encode/H.264
h264:
# Constant Rate Factor (CRF) 0-51 (default: 23)
crf: 26
crf: 23
# ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo
preset: superfast
# baseline, main, high, high10, high422, high444
Expand Down
1 change: 1 addition & 0 deletions pkg/config/emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type LibretroCoreConfig struct {
Height int
Hid map[int][]int
IsGlAllowed bool
KbMouseSupport bool
Lib string
Options map[string]string
Options4rom map[string]map[string]string // <(^_^)>
Expand Down
4 changes: 2 additions & 2 deletions pkg/coordinator/userapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ func (u *User) SendWebrtcOffer(sdp string) { u.Notify(api.WebrtcOffer, sdp) }
func (u *User) SendWebrtcIceCandidate(candidate string) { u.Notify(api.WebrtcIce, candidate) }

// StartGame signals the user that everything is ready to start a game.
func (u *User) StartGame(av *api.AppVideoInfo) {
u.Notify(api.StartGame, api.GameStartUserResponse{RoomId: u.w.RoomId, Av: av})
func (u *User) StartGame(av *api.AppVideoInfo, kbMouse bool) {
u.Notify(api.StartGame, api.GameStartUserResponse{RoomId: u.w.RoomId, Av: av, KbMouse: kbMouse})
}
2 changes: 1 addition & 1 deletion pkg/coordinator/userhandlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (u *User) HandleStartGame(rq api.GameStartUserRequest, launcher games.Launc
return
}
u.log.Info().Str("id", startGameResp.Rid).Msg("Received room response from worker")
u.StartGame(startGameResp.AV)
u.StartGame(startGameResp.AV, startGameResp.KbMouse)

// send back recording status
if conf.Recording.Enabled && rq.Record {
Expand Down
45 changes: 26 additions & 19 deletions pkg/network/webrtc/webrtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,15 @@ func (p *Peer) NewCall(vCodec, aCodec string, onICECandidate func(ice any)) (sdp
p.log.Debug().Msgf("Added [%s] track", audio.Codec().MimeType)
p.a = audio

// plug in the [data] channel (in and out)
if err = p.addDataChannel("data"); err != nil {
err = p.AddChannel("data", func(data []byte) {
if len(data) == 0 || p.OnMessage == nil {
return
}
p.OnMessage(data)
})
if err != nil {
return "", err
}
p.log.Debug().Msg("Added [data] chan")

p.conn.OnICEConnectionStateChange(p.handleICEState(func() { p.log.Info().Msg("Connected") }))
// Stream provider supposes to send offer
Expand Down Expand Up @@ -221,6 +225,19 @@ func (p *Peer) AddCandidate(candidate string, decoder Decoder) error {
return nil
}

func (p *Peer) AddChannel(label string, onMessage func([]byte)) error {
ch, err := p.addDataChannel(label)
if err != nil {
return err
}
if label == "data" {
p.d = ch
}
ch.OnMessage(func(m webrtc.DataChannelMessage) { onMessage(m.Data) })
p.log.Debug().Msgf("Added [%v] chan", label)
return nil
}

func (p *Peer) Disconnect() {
if p.conn == nil {
return
Expand All @@ -232,29 +249,19 @@ func (p *Peer) Disconnect() {
p.log.Debug().Msg("WebRTC stop")
}

// addDataChannel creates a new WebRTC data channel for user input.
// addDataChannel creates new WebRTC data channel.
// Default params -- ordered: true, negotiated: false.
func (p *Peer) addDataChannel(label string) error {
func (p *Peer) addDataChannel(label string) (*webrtc.DataChannel, error) {
ch, err := p.conn.CreateDataChannel(label, nil)
if err != nil {
return err
return nil, err
}
ch.OnOpen(func() {
p.log.Debug().Str("label", ch.Label()).Uint16("id", *ch.ID()).
Msg("Data channel [input] opened")
p.log.Debug().Uint16("id", *ch.ID()).Msgf("Data channel [%v] opened", ch.Label())
})
ch.OnError(p.logx)
ch.OnMessage(func(m webrtc.DataChannelMessage) {
if len(m.Data) == 0 {
return
}
if p.OnMessage != nil {
p.OnMessage(m.Data)
}
})
p.d = ch
ch.OnClose(func() { p.log.Debug().Msg("Data channel [input] has been closed") })
return nil
ch.OnClose(func() { p.log.Debug().Msgf("Data channel [%v] has been closed", ch.Label()) })
return ch, nil
}

func (p *Peer) logx(err error) { p.log.Error().Err(err) }
3 changes: 2 additions & 1 deletion pkg/worker/caged/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ type App interface {
SetAudioCb(func(Audio))
SetVideoCb(func(Video))
SetDataCb(func([]byte))
SendControl(port int, data []byte)
Input(port int, device byte, data []byte)
KbMouseSupport() bool
}

type Audio struct {
Expand Down
6 changes: 6 additions & 0 deletions pkg/worker/caged/caged.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ type Manager struct {
log *logger.Logger
}

const (
RetroPad = libretro.RetroPad
Keyboard = libretro.Keyboard
Mouse = libretro.Mouse
)

type ModName string

const Libretro ModName = "libretro"
Expand Down
25 changes: 13 additions & 12 deletions pkg/worker/caged/libretro/caged.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,16 @@ func (c *Caged) EnableCloudStorage(uid string, storage cloud.Storage) {
}
}

func (c *Caged) AspectEnabled() bool { return c.base.nano.Aspect }
func (c *Caged) AspectRatio() float32 { return c.base.AspectRatio() }
func (c *Caged) PixFormat() uint32 { return c.Emulator.PixFormat() }
func (c *Caged) Rotation() uint { return c.Emulator.Rotation() }
func (c *Caged) AudioSampleRate() int { return c.Emulator.AudioSampleRate() }
func (c *Caged) ViewportSize() (int, int) { return c.base.ViewportSize() }
func (c *Caged) Scale() float64 { return c.Emulator.Scale() }
func (c *Caged) SendControl(port int, data []byte) { c.base.Input(port, data) }
func (c *Caged) Start() { go c.Emulator.Start() }
func (c *Caged) SetSaveOnClose(v bool) { c.base.SaveOnClose = v }
func (c *Caged) SetSessionId(name string) { c.base.SetSessionId(name) }
func (c *Caged) Close() { c.Emulator.Close() }
func (c *Caged) AspectEnabled() bool { return c.base.nano.Aspect }
func (c *Caged) AspectRatio() float32 { return c.base.AspectRatio() }
func (c *Caged) PixFormat() uint32 { return c.Emulator.PixFormat() }
func (c *Caged) Rotation() uint { return c.Emulator.Rotation() }
func (c *Caged) AudioSampleRate() int { return c.Emulator.AudioSampleRate() }
func (c *Caged) ViewportSize() (int, int) { return c.base.ViewportSize() }
func (c *Caged) Scale() float64 { return c.Emulator.Scale() }
func (c *Caged) Input(p int, d byte, data []byte) { c.base.Input(p, d, data) }
func (c *Caged) KbMouseSupport() bool { return c.base.KbMouseSupport() }
func (c *Caged) Start() { go c.Emulator.Start() }
func (c *Caged) SetSaveOnClose(v bool) { c.base.SaveOnClose = v }
func (c *Caged) SetSessionId(name string) { c.base.SetSessionId(name) }
func (c *Caged) Close() { c.Emulator.Close() }
59 changes: 18 additions & 41 deletions pkg/worker/caged/libretro/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"path/filepath"
"sync"
"sync/atomic"
"time"
"unsafe"

Expand Down Expand Up @@ -44,15 +43,14 @@ type Emulator interface {
// Close will be called when the game is done
Close()
// Input passes input to the emulator
Input(player int, data []byte)
Input(player int, device byte, data []byte)
// Scale returns set video scale factor
Scale() float64
}

type Frontend struct {
conf config.Emulator
done chan struct{}
input InputState
log *logger.Logger
nano *nanoarch.Nanoarch
onAudio func(app.Audio)
Expand All @@ -70,21 +68,12 @@ type Frontend struct {
SaveOnClose bool
}

// InputState stores full controller state.
// It consists of:
// - uint16 button values
// - int16 analog stick values
type (
InputState [maxPort]State
State struct {
keys uint32
axes [dpadAxes]int32
}
)
type Device byte

const (
maxPort = 4
dpadAxes = 4
RetroPad = Device(nanoarch.RetroPad)
Keyboard = Device(nanoarch.Keyboard)
Mouse = Device(nanoarch.Mouse)
)

var (
Expand Down Expand Up @@ -129,7 +118,6 @@ func NewFrontend(conf config.Emulator, log *logger.Logger) (*Frontend, error) {
f := &Frontend{
conf: conf,
done: make(chan struct{}),
input: NewGameSessionInput(),
log: log,
onAudio: noAudio,
onData: noData,
Expand Down Expand Up @@ -162,6 +150,7 @@ func (f *Frontend) LoadCore(emu string) {
Options4rom: conf.Options4rom,
UsesLibCo: conf.UsesLibCo,
CoreAspectRatio: conf.CoreAspectRatio,
KbMouseSupport: conf.KbMouseSupport,
}
f.mu.Lock()
scale := 1.0
Expand Down Expand Up @@ -227,8 +216,6 @@ func (f *Frontend) linkNano(nano *nanoarch.Nanoarch) {
}
f.nano.WaitReady() // start only when nano is available

f.nano.OnKeyPress = f.input.isKeyPressed
f.nano.OnDpad = f.input.isDpadTouched
f.nano.OnVideo = f.handleVideo
f.nano.OnAudio = f.handleAudio
f.nano.OnDup = f.handleDup
Expand Down Expand Up @@ -300,8 +287,8 @@ func (f *Frontend) Flipped() bool { return f.nano.IsGL() }
func (f *Frontend) FrameSize() (int, int) { return f.nano.BaseWidth(), f.nano.BaseHeight() }
func (f *Frontend) HasSave() bool { return os.Exists(f.HashPath()) }
func (f *Frontend) HashPath() string { return f.storage.GetSavePath() }
func (f *Frontend) Input(player int, data []byte) { f.input.setInput(player, data) }
func (f *Frontend) IsPortrait() bool { return f.nano.IsPortrait() }
func (f *Frontend) KbMouseSupport() bool { return f.nano.KbMouseSupport() }
func (f *Frontend) LoadGame(path string) error { return f.nano.LoadGame(path) }
func (f *Frontend) PixFormat() uint32 { return f.nano.Video.PixFmt.C }
func (f *Frontend) RestoreGameState() error { return f.Load() }
Expand All @@ -318,6 +305,17 @@ func (f *Frontend) Tick() { f.mu.Lock(); f.nano.Run(); f
func (f *Frontend) ViewportRecalculate() { f.mu.Lock(); f.vw, f.vh = f.ViewportCalc(); f.mu.Unlock() }
func (f *Frontend) ViewportSize() (int, int) { return f.vw, f.vh }

func (f *Frontend) Input(port int, device byte, data []byte) {
switch Device(device) {
case RetroPad:
f.nano.InputRetropad(port, data)
case Keyboard:
f.nano.InputKeyboard(port, data)
case Mouse:
f.nano.InputMouse(port, data)
}
}

func (f *Frontend) ViewportCalc() (nw int, nh int) {
w, h := f.FrameSize()
nw, nh = w, h
Expand Down Expand Up @@ -408,24 +406,3 @@ func (f *Frontend) autosave(periodSec int) {
}
}
}

func NewGameSessionInput() InputState { return [maxPort]State{} }

// setInput sets input state for some player in a game session.
func (s *InputState) setInput(player int, data []byte) {
atomic.StoreUint32(&s[player].keys, uint32(uint16(data[1])<<8+uint16(data[0])))
for i, axes := 0, len(data); i < dpadAxes && i<<1+3 < axes; i++ {
axis := i<<1 + 2
atomic.StoreInt32(&s[player].axes[i], int32(data[axis+1])<<8+int32(data[axis]))
}
}

// isKeyPressed checks if some button is pressed by any player.
func (s *InputState) isKeyPressed(port uint, key int) int {
return int((atomic.LoadUint32(&s[port].keys) >> uint(key)) & 1)
}

// isDpadTouched checks if D-pad is used by any player.
func (s *InputState) isDpadTouched(port uint, axis uint) (shift int16) {
return int16(atomic.LoadInt32(&s[port].axes[axis]))
}
15 changes: 0 additions & 15 deletions pkg/worker/caged/libretro/frontend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ func EmulatorMock(room string, system string) *TestFrontend {
Path: os.TempDir(),
MainSave: room,
},
input: NewGameSessionInput(),
done: make(chan struct{}),
th: conf.Emulator.Threads,
log: l2,
Expand Down Expand Up @@ -340,20 +339,6 @@ func TestStateConcurrency(t *testing.T) {
}
}

func TestConcurrentInput(t *testing.T) {
var wg sync.WaitGroup
state := NewGameSessionInput()
events := 1000
wg.Add(2 * events)

for range events {
player := rand.IntN(maxPort)
go func() { state.setInput(player, []byte{0, 1}); wg.Done() }()
go func() { state.isKeyPressed(uint(player), 100); wg.Done() }()
}
wg.Wait()
}

func TestStartStop(t *testing.T) {
f1 := DefaultFrontend("sushi", sushi.system, sushi.rom)
go f1.Start()
Expand Down
Loading

0 comments on commit 0d78ef7

Please sign in to comment.