Swap mutex to atomics in keyboard input

This commit is contained in:
sergystepanov 2025-12-28 21:25:33 +03:00
parent 58a19affcb
commit 368bae8c07
3 changed files with 407 additions and 139 deletions

View file

@ -2,7 +2,6 @@ package nanoarch
import (
"encoding/binary"
"sync"
"sync/atomic"
)
@ -19,36 +18,11 @@ void input_cache_clear(void);
import "C"
const (
Released C.int16_t = iota
Pressed
maxPort = 4
numAxes = 4
RetrokLast = int(C.RETROK_LAST)
)
const RetrokLast = int(C.RETROK_LAST)
// InputState stores full controller state.
// It consists of:
// - uint16 button values
// - int16 analog stick values
type InputState [maxPort]RetroPadState
type (
RetroPadState struct {
keys uint32
axes [dpadAxes]int32
}
KeyboardState struct {
keys [RetrokLast]byte
mod uint16
mu sync.Mutex
}
MouseState struct {
dx, dy atomic.Int32
buttons atomic.Int32
}
)
type MouseBtnState int32
type Device byte
const (
@ -62,128 +36,110 @@ const (
MouseButton
)
type MouseBtnState int32
const (
MouseLeft MouseBtnState = 1 << iota
MouseRight
MouseMiddle
)
const (
maxPort = 4
dpadAxes = 4
)
// InputState stores controller state for all ports.
// - uint16 button bitmask
// - int16 analog axes x4
type InputState [maxPort]struct {
keys uint32
axes [numAxes]int32
}
// 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[port].axes[i], int32(data[axis+1])<<8+int32(data[axis]))
// SetInput sets input state for a player.
//
// [BTN:2][AX0:2][AX1:2][AX2:2][AX3:2]
func (s *InputState) SetInput(port int, data []byte) {
atomic.StoreUint32(&s[port].keys, uint32(binary.LittleEndian.Uint16(data)))
for i := 0; i < numAxes && i*2+3 < len(data); i++ {
atomic.StoreInt32(&s[port].axes[i], int32(int16(binary.LittleEndian.Uint16(data[i*2+2:]))))
}
}
// IsKeyPressed checks if some button is pressed by any player.
func (s *InputState) IsKeyPressed(port uint, key int) C.int16_t {
return C.int16_t((atomic.LoadUint32(&s[port].keys) >> uint(key)) & 1)
// Button check
func (s *InputState) Button(port, key uint) C.int16_t {
return C.int16_t((atomic.LoadUint32(&s[port].keys) >> key) & 1)
}
// IsDpadTouched checks if D-pad is used by any player.
func (s *InputState) IsDpadTouched(port uint, axis uint) (shift C.int16_t) {
return C.int16_t(atomic.LoadInt32(&s[port].axes[axis]))
}
// SyncToCache syncs the entire input state to the C-side cache.
// Call this once before each Run() instead of having C call back into Go.
// SyncToCache syncs input state to C-side cache before Run().
func (s *InputState) SyncToCache() {
for port := uint(0); port < maxPort; port++ {
buttons := atomic.LoadUint32(&s[port].keys)
axis0 := C.int16_t(atomic.LoadInt32(&s[port].axes[0]))
axis1 := C.int16_t(atomic.LoadInt32(&s[port].axes[1]))
axis2 := C.int16_t(atomic.LoadInt32(&s[port].axes[2]))
axis3 := C.int16_t(atomic.LoadInt32(&s[port].axes[3]))
C.input_cache_set_port(C.uint(port), C.uint32_t(buttons), axis0, axis1, axis2, axis3)
for p := uint(0); p < maxPort; p++ {
a := &s[p].axes
C.input_cache_set_port(C.uint(p), C.uint32_t(atomic.LoadUint32(&s[p].keys)),
C.int16_t(atomic.LoadInt32(&a[0])), C.int16_t(atomic.LoadInt32(&a[1])),
C.int16_t(atomic.LoadInt32(&a[2])), C.int16_t(atomic.LoadInt32(&a[3])))
}
}
// KeyboardState tracks keys of the keyboard.
type KeyboardState struct {
keys [6]atomic.Uint64 // 342 keys packed into 6 uint64s (384 bits)
mod atomic.Uint32
}
// SetKey sets keyboard state.
//
// 0 1 2 3 4 5 6
// [ KEY ] P MOD
// [KEY:4][P:1][MOD:2]
//
// KEY contains Libretro code of the keyboard key (4 bytes).
// P contains 0 or 1 if the key is pressed (1 byte).
// MOD contains bitmask for Alt | Ctrl | Meta | Shift keys press state (2 bytes).
//
// Returns decoded state from the input bytes.
// KEY - Libretro key code, P - pressed (0/1), MOD - modifier bitmask
func (ks *KeyboardState) SetKey(data []byte) (pressed bool, key uint, mod uint16) {
if len(data) != 7 {
return
}
press := data[4]
pressed = press == 1
key = uint(binary.BigEndian.Uint32(data))
mod = binary.BigEndian.Uint16(data[5:])
ks.mu.Lock()
ks.keys[key] = press
ks.mod = mod
ks.mu.Unlock()
pressed = data[4] == 1
idx, bit := key/64, uint64(1)<<(key%64)
if pressed {
ks.keys[idx].Or(bit)
} else {
ks.keys[idx].And(^bit)
}
ks.mod.Store(uint32(mod))
return
}
func (ks *KeyboardState) Pressed(key uint) C.int16_t {
ks.mu.Lock()
press := ks.keys[key]
ks.mu.Unlock()
if press == 1 {
return Pressed
}
return Released
}
// SyncToCache syncs keyboard state to the C-side cache.
// SyncToCache syncs keyboard state to C-side cache.
func (ks *KeyboardState) SyncToCache() {
ks.mu.Lock()
defer ks.mu.Unlock()
for id, pressed := range ks.keys {
for id := 0; id < RetrokLast; id++ {
pressed := (ks.keys[id/64].Load() >> (id % 64)) & 1
C.input_cache_set_keyboard_key(C.uint(id), C.uint8_t(pressed))
}
}
// ShiftPos sets mouse relative position state.
// MouseState tracks mouse delta and buttons.
type MouseState struct {
dx, dy atomic.Int32
buttons atomic.Int32
}
// ShiftPos adds relative mouse movement.
//
// 0 1 2 3
// [dx] [dy]
//
// dx and dy are relative mouse coordinates
// [dx:2][dy:2]
func (ms *MouseState) ShiftPos(data []byte) {
if len(data) != 4 {
return
}
dxy := binary.BigEndian.Uint32(data)
ms.dx.Add(int32(int16(dxy >> 16)))
ms.dy.Add(int32(int16(dxy)))
ms.dx.Add(int32(int16(binary.BigEndian.Uint16(data[:2]))))
ms.dy.Add(int32(int16(binary.BigEndian.Uint16(data[2:]))))
}
func (ms *MouseState) PopX() C.int16_t { return C.int16_t(ms.dx.Swap(0)) }
func (ms *MouseState) PopY() C.int16_t { return C.int16_t(ms.dy.Swap(0)) }
// SetButtons sets the state MouseBtnState of mouse buttons.
func (ms *MouseState) SetButtons(data byte) { ms.buttons.Store(int32(data)) }
func (ms *MouseState) SetButtons(b byte) { ms.buttons.Store(int32(b)) }
func (ms *MouseState) Buttons() (l, r, m bool) {
mbs := MouseBtnState(ms.buttons.Load())
l = mbs&MouseLeft != 0
r = mbs&MouseRight != 0
m = mbs&MouseMiddle != 0
return
b := MouseBtnState(ms.buttons.Load())
return b&MouseLeft != 0, b&MouseRight != 0, b&MouseMiddle != 0
}
// SyncToCache syncs mouse state to the C-side cache.
// This consumes the delta values (swaps to 0).
// SyncToCache syncs mouse state to C-side cache, consuming deltas.
func (ms *MouseState) SyncToCache() {
dx := C.int16_t(ms.dx.Swap(0))
dy := C.int16_t(ms.dy.Swap(0))
buttons := C.uint8_t(ms.buttons.Load())
C.input_cache_set_mouse(dx, dy, buttons)
C.input_cache_set_mouse(C.int16_t(ms.dx.Swap(0)), C.int16_t(ms.dy.Swap(0)), C.uint8_t(ms.buttons.Load()))
}

View file

@ -7,21 +7,215 @@ import (
"testing"
)
func TestConcurrentInput(t *testing.T) {
func TestInputState_SetInput(t *testing.T) {
tests := []struct {
name string
port int
data []byte
keys uint32
axes [4]int32
}{
{
name: "buttons only",
port: 0,
data: []byte{0xFF, 0x01},
keys: 0x01FF,
},
{
name: "buttons and axes",
port: 1,
data: []byte{0x03, 0x00, 0x10, 0x27, 0xF0, 0xD8, 0x00, 0x80, 0xFF, 0x7F},
keys: 0x0003,
axes: [4]int32{10000, -10000, -32768, 32767},
},
{
name: "partial axes",
port: 2,
data: []byte{0x01, 0x00, 0x64, 0x00},
keys: 0x0001,
axes: [4]int32{100, 0, 0, 0},
},
{
name: "max port",
port: 3,
data: []byte{0xFF, 0xFF},
keys: 0xFFFF,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
state := InputState{}
state.SetInput(test.port, test.data)
if state[test.port].keys != test.keys {
t.Errorf("keys: got %v, want %v", state[test.port].keys, test.keys)
}
for i, want := range test.axes {
if state[test.port].axes[i] != want {
t.Errorf("axes[%d]: got %v, want %v", i, state[test.port].axes[i], want)
}
}
})
}
}
func TestInputState_Concurrent(t *testing.T) {
var wg sync.WaitGroup
state := InputState{}
events := 1000
wg.Add(2 * events)
wg.Add(events)
for range events {
player := rand.Intn(maxPort)
go func() { state.Input(player, []byte{0, 1}); wg.Done() }()
go func() { state.IsKeyPressed(uint(player), 100); wg.Done() }()
go func() {
state.SetInput(player, []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0})
wg.Done()
}()
}
wg.Wait()
}
func TestMousePos(t *testing.T) {
func TestKeyboardState_SetKey(t *testing.T) {
tests := []struct {
name string
data []byte
pressed bool
key uint
mod uint16
}{
{
name: "key pressed",
data: []byte{0, 0, 0, 42, 1, 0, 3},
pressed: true,
key: 42,
mod: 3,
},
{
name: "key released",
data: []byte{0, 0, 0, 100, 0, 0, 0},
pressed: false,
key: 100,
mod: 0,
},
{
name: "high key code",
data: []byte{0, 0, 1, 50, 1, 0xFF, 0xFF},
pressed: true,
key: 306,
mod: 0xFFFF,
},
{
name: "invalid length",
data: []byte{0, 0, 0},
pressed: false,
key: 0,
mod: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ks := KeyboardState{}
pressed, key, mod := ks.SetKey(test.data)
if pressed != test.pressed {
t.Errorf("pressed: got %v, want %v", pressed, test.pressed)
}
if key != test.key {
t.Errorf("key: got %v, want %v", key, test.key)
}
if mod != test.mod {
t.Errorf("mod: got %v, want %v", mod, test.mod)
}
})
}
}
func TestKeyboardState_IsPressed(t *testing.T) {
ks := KeyboardState{}
// Initially not pressed
if ks.keys[0].Load() != 0 {
t.Error("key should not be pressed initially")
}
// Press key
ks.SetKey([]byte{0, 0, 0, 42, 1, 0, 0})
if (ks.keys[42/64].Load()>>(42%64))&1 != 1 {
t.Error("key should be pressed")
}
// Release key
ks.SetKey([]byte{0, 0, 0, 42, 0, 0, 0})
if (ks.keys[42/64].Load()>>(42%64))&1 != 0 {
t.Error("key should be released")
}
}
func TestKeyboardState_MultipleBits(t *testing.T) {
ks := KeyboardState{}
// Press keys in different uint64 slots
keys := []uint{0, 63, 64, 127, 128, 200, 300, 341}
for _, k := range keys {
data := make([]byte, 7)
binary.BigEndian.PutUint32(data, uint32(k))
data[4] = 1
ks.SetKey(data)
}
// Check all pressed
for _, k := range keys {
if (ks.keys[k/64].Load()>>(k%64))&1 != 1 {
t.Errorf("key %d should be pressed", k)
}
}
// Release some
for _, k := range []uint{0, 128, 341} {
data := make([]byte, 7)
binary.BigEndian.PutUint32(data, uint32(k))
data[4] = 0
ks.SetKey(data)
}
// Check states
expected := map[uint]uint64{
0: 0, 63: 1, 64: 1, 127: 1, 128: 0, 200: 1, 300: 1, 341: 0,
}
for k, want := range expected {
got := (ks.keys[k/64].Load() >> (k % 64)) & 1
if got != want {
t.Errorf("key %d: got %v, want %v", k, got, want)
}
}
}
func TestKeyboardState_Concurrent(t *testing.T) {
var wg sync.WaitGroup
ks := KeyboardState{}
events := 1000
wg.Add(events * 2)
for range events {
key := uint(rand.Intn(RetrokLast))
go func() {
data := make([]byte, 7)
binary.BigEndian.PutUint32(data, uint32(key))
data[4] = byte(rand.Intn(2))
ks.SetKey(data)
wg.Done()
}()
go func() {
_ = (ks.keys[key/64].Load() >> (key % 64)) & 1
wg.Done()
}()
}
wg.Wait()
}
func TestMouseState_ShiftPos(t *testing.T) {
tests := []struct {
name string
dx int16
@ -30,42 +224,109 @@ func TestMousePos(t *testing.T) {
ry int16
b func(dx, dy int16) []byte
}{
{name: "normal", dx: -10123, dy: 5678, rx: -10123, ry: 5678, b: func(dx, dy int16) []byte {
data := []byte{0, 0, 0, 0}
binary.BigEndian.PutUint16(data, uint16(dx))
binary.BigEndian.PutUint16(data[2:], uint16(dy))
return data
}},
{name: "wrong endian", dx: -1234, dy: 5678, rx: 12027, ry: 11798, b: func(dx, dy int16) []byte {
data := []byte{0, 0, 0, 0}
binary.LittleEndian.PutUint16(data, uint16(dx))
binary.LittleEndian.PutUint16(data[2:], uint16(dy))
return data
}},
{
name: "positive values",
dx: 100,
dy: 200,
rx: 100,
ry: 200,
b: func(dx, dy int16) []byte {
data := make([]byte, 4)
binary.BigEndian.PutUint16(data, uint16(dx))
binary.BigEndian.PutUint16(data[2:], uint16(dy))
return data
},
},
{
name: "negative values",
dx: -10123,
dy: 5678,
rx: -10123,
ry: 5678,
b: func(dx, dy int16) []byte {
data := make([]byte, 4)
binary.BigEndian.PutUint16(data, uint16(dx))
binary.BigEndian.PutUint16(data[2:], uint16(dy))
return data
},
},
{
name: "wrong endian",
dx: -1234,
dy: 5678,
rx: 12027,
ry: 11798,
b: func(dx, dy int16) []byte {
data := make([]byte, 4)
binary.LittleEndian.PutUint16(data, uint16(dx))
binary.LittleEndian.PutUint16(data[2:], uint16(dy))
return data
},
},
{
name: "max values",
dx: 32767,
dy: -32768,
rx: 32767,
ry: -32768,
b: func(dx, dy int16) []byte {
data := make([]byte, 4)
binary.BigEndian.PutUint16(data, uint16(dx))
binary.BigEndian.PutUint16(data[2:], uint16(dy))
return data
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
data := test.b(test.dx, test.dy)
ms := MouseState{}
ms.ShiftPos(data)
ms.ShiftPos(test.b(test.dx, test.dy))
x := int16(ms.PopX())
y := int16(ms.PopY())
x, y := int16(ms.dx.Swap(0)), int16(ms.dy.Swap(0))
if x != test.rx || y != test.ry {
t.Errorf("invalid state, %v = %v, %v = %v", test.rx, x, test.ry, y)
t.Errorf("got (%v, %v), want (%v, %v)", x, y, test.rx, test.ry)
}
if ms.dx.Load() != 0 || ms.dy.Load() != 0 {
t.Errorf("coordinates weren't cleared")
t.Error("coordinates weren't cleared")
}
})
}
}
func TestMouseButtons(t *testing.T) {
func TestMouseState_ShiftPosAccumulates(t *testing.T) {
ms := MouseState{}
data := make([]byte, 4)
binary.BigEndian.PutUint16(data, uint16(10))
binary.BigEndian.PutUint16(data[2:], uint16(20))
ms.ShiftPos(data)
ms.ShiftPos(data)
ms.ShiftPos(data)
if got := ms.dx.Load(); got != 30 {
t.Errorf("dx: got %v, want 30", got)
}
if got := ms.dy.Load(); got != 60 {
t.Errorf("dy: got %v, want 60", got)
}
}
func TestMouseState_ShiftPosInvalidLength(t *testing.T) {
ms := MouseState{}
ms.ShiftPos([]byte{1, 2, 3})
ms.ShiftPos([]byte{1, 2, 3, 4, 5})
if ms.dx.Load() != 0 || ms.dy.Load() != 0 {
t.Error("invalid data should be ignored")
}
}
func TestMouseState_Buttons(t *testing.T) {
tests := []struct {
name string
data byte
@ -73,10 +334,13 @@ func TestMouseButtons(t *testing.T) {
r bool
m bool
}{
{name: "l+r+m+", data: 1 + 2 + 4, l: true, r: true, m: true},
{name: "l-r-m-", data: 0},
{name: "l-r+m-", data: 2, r: true},
{name: "l+r-m+", data: 1 + 4, l: true, m: true},
{name: "none", data: 0},
{name: "left", data: 1, l: true},
{name: "right", data: 2, r: true},
{name: "middle", data: 4, m: true},
{name: "left+right", data: 3, l: true, r: true},
{name: "all", data: 7, l: true, r: true, m: true},
{name: "left+middle", data: 5, l: true, m: true},
}
ms := MouseState{}
@ -86,8 +350,56 @@ func TestMouseButtons(t *testing.T) {
ms.SetButtons(test.data)
l, r, m := ms.Buttons()
if l != test.l || r != test.r || m != test.m {
t.Errorf("wrong button state: %v -> %v, %v, %v", test.data, l, r, m)
t.Errorf("got (%v, %v, %v), want (%v, %v, %v)", l, r, m, test.l, test.r, test.m)
}
})
}
}
func TestMouseState_Concurrent(t *testing.T) {
var wg sync.WaitGroup
ms := MouseState{}
events := 1000
wg.Add(events * 3)
for range events {
go func() {
data := make([]byte, 4)
binary.BigEndian.PutUint16(data, uint16(rand.Int31n(100)-50))
binary.BigEndian.PutUint16(data[2:], uint16(rand.Int31n(100)-50))
ms.ShiftPos(data)
wg.Done()
}()
go func() {
ms.SetButtons(byte(rand.Intn(8)))
wg.Done()
}()
go func() {
ms.Buttons()
wg.Done()
}()
}
wg.Wait()
}
func TestConstants(t *testing.T) {
// MouseBtnState
if MouseLeft != 1 || MouseRight != 2 || MouseMiddle != 4 {
t.Error("invalid MouseBtnState constants")
}
// Device
if RetroPad != 0 || Keyboard != 1 || Mouse != 2 {
t.Error("invalid Device constants")
}
// Mouse events
if MouseMove != 0 || MouseButton != 1 {
t.Error("invalid mouse event constants")
}
// Limits
if maxPort != 4 || numAxes != 4 || RetrokLast != 342 {
t.Error("invalid limit constants")
}
}

View file

@ -426,7 +426,7 @@ func (n *Nanoarch) Run() {
func (n *Nanoarch) IsSupported() error { return graphics.TryInit() }
func (n *Nanoarch) IsGL() bool { return n.Video.gl.enabled }
func (n *Nanoarch) IsStopped() bool { return n.Stopped.Load() }
func (n *Nanoarch) InputRetropad(port int, data []byte) { n.retropad.Input(port, data) }
func (n *Nanoarch) InputRetropad(port int, data []byte) { n.retropad.SetInput(port, data) }
func (n *Nanoarch) InputKeyboard(_ int, data []byte) {
if n.keyboardCb == nil {
return