Skip to content

Commit

Permalink
Implement keyboard controls
Browse files Browse the repository at this point in the history
  • Loading branch information
sergystepanov committed Feb 25, 2024
1 parent 59e96c9 commit d013391
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 133 deletions.
4 changes: 2 additions & 2 deletions pkg/worker/caged/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ type App interface {
SetAudioCb(func(Audio))
SetVideoCb(func(Video))
SetDataCb(func([]byte))
SendControl(port int, data []byte)
SendKeyboardKey(port int, down bool, key uint)
InputGamepad(port int, data []byte)
InputKeyboard(port int, data []byte)
}

type Audio struct {
Expand Down
26 changes: 13 additions & 13 deletions pkg/worker/caged/libretro/caged.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +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) SendKeyboardKey(port int, down bool, key uint) { c.base.KeyboardEvent(down, key, 0, 0) }
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) InputGamepad(port int, data []byte) { c.base.Input(port, RetroPad, data) }
func (c *Caged) InputKeyboard(port int, data []byte) { c.base.Input(port, Keyboard, 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() }
66 changes: 36 additions & 30 deletions pkg/worker/caged/libretro/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,23 @@ 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, d Device, data []byte)
// Scale returns set video scale factor
Scale() float64
KeyboardEvent(down bool, keycode uint, character uint32, keyModifiers uint16)
}

type Frontend struct {
conf config.Emulator
done chan struct{}
retropad nanoarch.InputState
log *logger.Logger
nano *nanoarch.Nanoarch
onAudio func(app.Audio)
onData func([]byte)
onVideo func(app.Video)
storage Storage
scale float64
th int // draw threads
vw, vh int // out frame size
conf config.Emulator
done chan struct{}
log *logger.Logger
nano *nanoarch.Nanoarch
onAudio func(app.Audio)
onData func([]byte)
onVideo func(app.Video)
storage Storage
scale float64
th int // draw threads
vw, vh int // out frame size

mu sync.Mutex
mui sync.Mutex
Expand All @@ -70,6 +68,13 @@ type Frontend struct {
SaveOnClose bool
}

type Device byte

const (
RetroPad = Device(nanoarch.RetroPad)
Keyboard = Device(nanoarch.Keyboard)
)

var (
audioPool sync.Pool
noAudio = func(app.Audio) {}
Expand Down Expand Up @@ -114,15 +119,14 @@ func NewFrontend(conf config.Emulator, log *logger.Logger) (*Frontend, error) {

// set global link to the Libretro
f := &Frontend{
conf: conf,
done: make(chan struct{}),
retropad: nanoarch.NewRetroPadState(),
log: log,
onAudio: noAudio,
onData: noData,
onVideo: noVideo,
storage: store,
th: conf.Threads,
conf: conf,
done: make(chan struct{}),
log: log,
onAudio: noAudio,
onData: noData,
onVideo: noVideo,
storage: store,
th: conf.Threads,
}
f.linkNano(nano)

Expand Down Expand Up @@ -203,8 +207,6 @@ func (f *Frontend) linkNano(nano *nanoarch.Nanoarch) {
}
f.nano.WaitReady() // start only when nano is available

f.nano.OnKeyPress = f.retropad.IsKeyPressed
f.nano.OnDpad = f.retropad.IsDpadTouched
f.nano.OnVideo = f.handleVideo
f.nano.OnAudio = f.handleAudio
}
Expand Down Expand Up @@ -275,7 +277,6 @@ 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.retropad.SetInput(player, data) }
func (f *Frontend) IsPortrait() bool { return f.nano.IsPortrait() }
func (f *Frontend) LoadGame(path string) error { return f.nano.LoadGame(path) }
func (f *Frontend) PixFormat() uint32 { return f.nano.Video.PixFmt.C }
Expand All @@ -293,6 +294,15 @@ 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, d Device, data []byte) {
switch d {
case RetroPad:
f.nano.InputRetropad(port, data)
case Keyboard:
f.nano.InputKeyboard(port, data)
}
}

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

func (f *Frontend) KeyboardEvent(down bool, key uint, char uint32, mod uint16) {
f.nano.KeyboardEvent(down, key, char, mod)
}
1 change: 0 additions & 1 deletion pkg/worker/caged/libretro/frontend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ func EmulatorMock(room string, system string) *TestFrontend {
Path: os.TempDir(),
MainSave: room,
},
retropad: nanoarch.NewRetroPadState(),
done: make(chan struct{}),
th: conf.Emulator.Threads,
log: l2,
Expand Down
75 changes: 66 additions & 9 deletions pkg/worker/caged/libretro/nanoarch/input.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
package nanoarch

import "sync/atomic"
import (
"encoding/binary"
"sync"
"sync/atomic"
)

//#include <stdint.h>
import "C"

const KeyPressed = C.int16_t(1)
const KeyReleased = C.int16_t(0)

const RetroDeviceTypeShift = 8

Expand All @@ -14,6 +24,18 @@ type (
keys uint32
axes [dpadAxes]int32
}
KeyboardState struct {
keys map[uint]struct{}
mod uint16
mu sync.Mutex
}
)

type Device byte

const (
RetroPad Device = iota
Keyboard
)

const (
Expand All @@ -23,23 +45,58 @@ const (

func NewRetroPadState() InputState { return [maxPort]RetroPadState{} }

// 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])))
// Input sets input state for some player in a game session.
func (s *InputState) Input(port int, data []byte) {
atomic.StoreUint32(&s[port].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]))
atomic.StoreInt32(&s[port].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)
func (s *InputState) IsKeyPressed(port uint, key int) C.int16_t {
return C.int16_t((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]))
func (s *InputState) IsDpadTouched(port uint, axis uint) (shift C.int16_t) {
return C.int16_t(atomic.LoadInt32(&s[port].axes[axis]))
}

func RetroDeviceSubclass(base, id int) int { return ((id + 1) << RetroDeviceTypeShift) | base }

func NewKeyboardState() KeyboardState {
return KeyboardState{
keys: make(map[uint]struct{}),
mod: 0,
}
}

func (ks *KeyboardState) Set(press bool, key uint, mod uint16) {
ks.mu.Lock()
if press {
ks.keys[key] = struct{}{}
} else {
delete(ks.keys, key)
}
ks.mod = mod
ks.mu.Unlock()
}

func (ks *KeyboardState) Pressed(key uint) C.int16_t {
ks.mu.Lock()
_, ok := ks.keys[key]
ks.mu.Unlock()
if ok {
return KeyPressed
}
return KeyReleased
}

func decodeKeyboardState(data []byte) (bool, uint, uint16) {
press := data[4] == 1
key := uint(binary.LittleEndian.Uint32(data))
mod := binary.LittleEndian.Uint16(data[5:])
return press, key, mod
}
2 changes: 1 addition & 1 deletion pkg/worker/caged/libretro/nanoarch/input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestConcurrentInput(t *testing.T) {

for i := 0; i < events; i++ {
player := rand.Intn(maxPort)
go func() { state.SetInput(player, []byte{0, 1}); wg.Done() }()
go func() { state.Input(player, []byte{0, 1}); wg.Done() }()
go func() { state.IsKeyPressed(uint(player), 100); wg.Done() }()
}
wg.Wait()
Expand Down
Loading

0 comments on commit d013391

Please sign in to comment.