diff --git a/pkg/worker/caged/libretro/nanoarch/input.go b/pkg/worker/caged/libretro/nanoarch/input.go index 59b5b014..eb6080c5 100644 --- a/pkg/worker/caged/libretro/nanoarch/input.go +++ b/pkg/worker/caged/libretro/nanoarch/input.go @@ -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)) } } diff --git a/pkg/worker/caged/libretro/nanoarch/input_test.go b/pkg/worker/caged/libretro/nanoarch/input_test.go index 4cda73f1..1df81da7 100644 --- a/pkg/worker/caged/libretro/nanoarch/input_test.go +++ b/pkg/worker/caged/libretro/nanoarch/input_test.go @@ -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() }() } diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.c b/pkg/worker/caged/libretro/nanoarch/nanoarch.c index d9010539..63d3b4d4 100644 --- a/pkg/worker/caged/libretro/nanoarch/nanoarch.c +++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.c @@ -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: