Add analog triggers and pack axes into atomic int64

- Pack 4 analog axes (LX, LY, RX, RY) into single int64 for atomic access
- Pack L2/R2 analog triggers into single int32
- Reduce memory per port from 20 to 16 bytes
- Reduce atomic stores per SetInput from 5 to 3
- Add RETRO_DEVICE_INDEX_ANALOG_BUTTON support for analog trigger queries
- Fallback to digital (0/0x7FFF) for non-trigger analog button queries

Wire format: [BTN:2][LX:2][LY:2][RX:2][RY:2][L2:2][R2:2] (14 bytes)
This commit is contained in:
sergystepanov 2025-12-29 20:20:55 +03:00
parent 368bae8c07
commit 1d5bae0c62
3 changed files with 188 additions and 44 deletions

View file

@ -10,7 +10,8 @@ import (
#include "libretro.h"
void input_cache_set_port(unsigned port, uint32_t buttons,
int16_t axis0, int16_t axis1, int16_t axis2, int16_t axis3);
int16_t lx, int16_t ly, int16_t rx, int16_t ry,
int16_t l2, int16_t r2);
void input_cache_set_keyboard_key(unsigned id, uint8_t pressed);
void input_cache_set_mouse(int16_t dx, int16_t dy, uint8_t buttons);
void input_cache_clear(void);
@ -46,34 +47,55 @@ const (
// InputState stores controller state for all ports.
// - uint16 button bitmask
// - int16 analog axes x4
// - int16 analog axes x4 (left stick, right stick)
// - int16 analog triggers x2 (L2, R2)
type InputState [maxPort]struct {
keys uint32
axes [numAxes]int32
keys uint32 // lower 16 bits used
axes int64 // packed: [LX:16][LY:16][RX:16][RY:16]
triggers int32 // packed: [L2:16][R2:16]
}
// SetInput sets input state for a player.
//
// [BTN:2][AX0:2][AX1:2][AX2:2][AX3:2]
// [BTN:2][LX:2][LY:2][RX:2][RY:2][L2:2][R2: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:]))))
if len(data) < 2 {
return
}
}
// Button check
func (s *InputState) Button(port, key uint) C.int16_t {
return C.int16_t((atomic.LoadUint32(&s[port].keys) >> key) & 1)
// Buttons
atomic.StoreUint32(&s[port].keys, uint32(binary.LittleEndian.Uint16(data)))
// Axes - pack into int64
var packedAxes int64
for i := 0; i < numAxes && i*2+3 < len(data); i++ {
axis := int64(int16(binary.LittleEndian.Uint16(data[i*2+2:])))
packedAxes |= (axis & 0xFFFF) << (i * 16)
}
atomic.StoreInt64(&s[port].axes, packedAxes)
// Analog triggers L2, R2 - pack into int32
if len(data) >= 14 {
l2 := int32(int16(binary.LittleEndian.Uint16(data[10:])))
r2 := int32(int16(binary.LittleEndian.Uint16(data[12:])))
atomic.StoreInt32(&s[port].triggers, (l2&0xFFFF)|((r2&0xFFFF)<<16))
}
}
// SyncToCache syncs input state to C-side cache before Run().
func (s *InputState) SyncToCache() {
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])))
keys := atomic.LoadUint32(&s[p].keys)
axes := atomic.LoadInt64(&s[p].axes)
triggers := atomic.LoadInt32(&s[p].triggers)
C.input_cache_set_port(C.uint(p), C.uint32_t(keys),
C.int16_t(axes),
C.int16_t(axes>>16),
C.int16_t(axes>>32),
C.int16_t(axes>>48),
C.int16_t(triggers),
C.int16_t(triggers>>16))
}
}

View file

@ -9,11 +9,12 @@ import (
func TestInputState_SetInput(t *testing.T) {
tests := []struct {
name string
port int
data []byte
keys uint32
axes [4]int32
name string
port int
data []byte
keys uint32
axes [4]int16
triggers [2]int16
}{
{
name: "buttons only",
@ -26,14 +27,14 @@ func TestInputState_SetInput(t *testing.T) {
port: 1,
data: []byte{0x03, 0x00, 0x10, 0x27, 0xF0, 0xD8, 0x00, 0x80, 0xFF, 0x7F},
keys: 0x0003,
axes: [4]int32{10000, -10000, -32768, 32767},
axes: [4]int16{10000, -10000, -32768, 32767},
},
{
name: "partial axes",
port: 2,
data: []byte{0x01, 0x00, 0x64, 0x00},
keys: 0x0001,
axes: [4]int32{100, 0, 0, 0},
axes: [4]int16{100, 0, 0, 0},
},
{
name: "max port",
@ -41,6 +42,46 @@ func TestInputState_SetInput(t *testing.T) {
data: []byte{0xFF, 0xFF},
keys: 0xFFFF,
},
{
name: "full input with triggers",
port: 0,
data: []byte{
0x03, 0x00, // buttons
0x10, 0x27, // LX: 10000
0xF0, 0xD8, // LY: -10000
0x00, 0x80, // RX: -32768
0xFF, 0x7F, // RY: 32767
0xFF, 0x3F, // L2: 16383
0xFF, 0x7F, // R2: 32767
},
keys: 0x0003,
axes: [4]int16{10000, -10000, -32768, 32767},
triggers: [2]int16{16383, 32767},
},
{
name: "axes without triggers",
port: 1,
data: []byte{
0x01, 0x00,
0x64, 0x00, // LX: 100
0xC8, 0x00, // LY: 200
0x2C, 0x01, // RX: 300
0x90, 0x01, // RY: 400
},
keys: 0x0001,
axes: [4]int16{100, 200, 300, 400},
},
{
name: "zero triggers",
port: 2,
data: []byte{
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, // L2: 0
0x00, 0x00, // R2: 0
},
keys: 0x0000,
},
}
for _, test := range tests {
@ -51,15 +92,82 @@ func TestInputState_SetInput(t *testing.T) {
if state[test.port].keys != test.keys {
t.Errorf("keys: got %v, want %v", state[test.port].keys, test.keys)
}
// Check axes from packed int64
axes := state[test.port].axes
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)
got := int16(axes >> (i * 16))
if got != want {
t.Errorf("axes[%d]: got %v, want %v", i, got, want)
}
}
// Check triggers from packed int32
triggers := state[test.port].triggers
l2 := int16(triggers)
r2 := int16(triggers >> 16)
if l2 != test.triggers[0] {
t.Errorf("L2: got %v, want %v", l2, test.triggers[0])
}
if r2 != test.triggers[1] {
t.Errorf("R2: got %v, want %v", r2, test.triggers[1])
}
})
}
}
func TestInputState_AxisExtraction(t *testing.T) {
state := InputState{}
data := []byte{
0x00, 0x00, // buttons
0x01, 0x00, // LX: 1
0x02, 0x00, // LY: 2
0x03, 0x00, // RX: 3
0x04, 0x00, // RY: 4
0x05, 0x00, // L2: 5
0x06, 0x00, // R2: 6
}
state.SetInput(0, data)
axes := state[0].axes
expected := []int16{1, 2, 3, 4}
for i, want := range expected {
got := int16(axes >> (i * 16))
if got != want {
t.Errorf("axis[%d]: got %v, want %v", i, got, want)
}
}
triggers := state[0].triggers
if got := int16(triggers); got != 5 {
t.Errorf("L2: got %v, want 5", got)
}
if got := int16(triggers >> 16); got != 6 {
t.Errorf("R2: got %v, want 6", got)
}
}
func TestInputState_NegativeAxes(t *testing.T) {
state := InputState{}
data := []byte{
0x00, 0x00, // buttons
0x00, 0x80, // LX: -32768
0xFF, 0xFF, // LY: -1
0x01, 0x80, // RX: -32767
0xFE, 0xFF, // RY: -2
}
state.SetInput(0, data)
axes := state[0].axes
expected := []int16{-32768, -1, -32767, -2}
for i, want := range expected {
got := int16(axes >> (i * 16))
if got != want {
t.Errorf("axis[%d]: got %v, want %v", i, got, want)
}
}
}
func TestInputState_Concurrent(t *testing.T) {
var wg sync.WaitGroup
state := InputState{}
@ -69,7 +177,8 @@ func TestInputState_Concurrent(t *testing.T) {
for range events {
player := rand.Intn(maxPort)
go func() {
state.SetInput(player, []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0})
// Full 14-byte input
state.SetInput(player, []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
wg.Done()
}()
}

View file

@ -33,30 +33,30 @@ void *same_thread_with_args(void *f, int type, ...);
#define INPUT_MAX_KEYS 512
typedef struct {
// Retropad: store raw button bitmask and analog axes per port
uint32_t buttons[INPUT_MAX_PORTS];
int16_t analog[INPUT_MAX_PORTS][4]; // 4 axes per port
int16_t analog[INPUT_MAX_PORTS][4]; // LX, LY, RX, RY
int16_t triggers[INPUT_MAX_PORTS][2]; // L2, R2
// Keyboard
uint8_t keyboard[INPUT_MAX_KEYS];
// Mouse
int16_t mouse_x;
int16_t mouse_y;
uint8_t mouse_buttons; // bit 0=left, bit 1=right, bit 2=middle
uint8_t mouse_buttons;
} input_cache_t;
static input_cache_t input_cache = {0};
// Update entire port state at once
void input_cache_set_port(unsigned port, uint32_t buttons,
int16_t axis0, int16_t axis1, int16_t axis2, int16_t axis3) {
int16_t lx, int16_t ly, int16_t rx, int16_t ry,
int16_t l2, int16_t r2) {
if (port < INPUT_MAX_PORTS) {
input_cache.buttons[port] = buttons;
input_cache.analog[port][0] = axis0;
input_cache.analog[port][1] = axis1;
input_cache.analog[port][2] = axis2;
input_cache.analog[port][3] = axis3;
input_cache.analog[port][0] = lx;
input_cache.analog[port][1] = ly;
input_cache.analog[port][2] = rx;
input_cache.analog[port][3] = ry;
input_cache.triggers[port][0] = l2;
input_cache.triggers[port][1] = r2;
}
}
@ -202,23 +202,36 @@ int16_t core_input_state_cgo(unsigned port, unsigned device, unsigned index, uns
switch (device) {
case RETRO_DEVICE_JOYPAD:
// Extract button bit from cached bitmask
return (int16_t)((input_cache.buttons[port] >> id) & 1);
case RETRO_DEVICE_ANALOG:
switch (index) {
case RETRO_DEVICE_INDEX_ANALOG_LEFT:
// id: 0=X, 1=Y
if (id < 2) {
// id: RETRO_DEVICE_ID_ANALOG_X=0, RETRO_DEVICE_ID_ANALOG_Y=1
if (id <= RETRO_DEVICE_ID_ANALOG_Y) {
return input_cache.analog[port][id];
}
break;
case RETRO_DEVICE_INDEX_ANALOG_RIGHT:
// id: 0=X, 1=Y -> stored in axes[2], axes[3]
if (id < 2) {
// id: RETRO_DEVICE_ID_ANALOG_X=0, RETRO_DEVICE_ID_ANALOG_Y=1
if (id <= RETRO_DEVICE_ID_ANALOG_Y) {
return input_cache.analog[port][2 + id];
}
break;
case RETRO_DEVICE_INDEX_ANALOG_BUTTON:
// Any button can be queried as analog
// id = RETRO_DEVICE_ID_JOYPAD_* (0-15)
// For now, only L2/R2 have analog values
switch (id) {
case RETRO_DEVICE_ID_JOYPAD_L2:
return input_cache.triggers[port][0];
case RETRO_DEVICE_ID_JOYPAD_R2:
return input_cache.triggers[port][1];
default:
// Other buttons: return digital as 0 or 0x7fff
return ((input_cache.buttons[port] >> id) & 1) ? 0x7FFF : 0;
}
break;
}
break;
@ -232,12 +245,12 @@ int16_t core_input_state_cgo(unsigned port, unsigned device, unsigned index, uns
switch (id) {
case RETRO_DEVICE_ID_MOUSE_X: {
int16_t x = input_cache.mouse_x;
input_cache.mouse_x = 0; // Consume delta
input_cache.mouse_x = 0;
return x;
}
case RETRO_DEVICE_ID_MOUSE_Y: {
int16_t y = input_cache.mouse_y;
input_cache.mouse_y = 0; // Consume delta
input_cache.mouse_y = 0;
return y;
}
case RETRO_DEVICE_ID_MOUSE_LEFT: