mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-22 18:25:37 +00:00
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:
parent
368bae8c07
commit
1d5bae0c62
3 changed files with 188 additions and 44 deletions
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue