Test JogCon steering, hotplug and live input

This commit is contained in:
Shokunin
2026-10-02 18:05:12 -07:00
parent 3df760f524
commit b44c72349d
10 changed files with 161 additions and 28 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring |
| `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) |
| `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint |
| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames |
| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry`, `pad_type` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Debug builds also accept `pad_type` (0 digital, 1 DualShock, 2 JogCon) to test an emulated device identity. Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames |
| `clear_input` | ✓ | ✓ | — | Remove input and analog axis overrides |
| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation |
| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state |
+3
View File
@@ -392,6 +392,9 @@ if(BUILD_TESTING)
add_executable(controller_type_test tests/test_controller_type.cpp)
target_include_directories(controller_type_test PRIVATE include)
add_test(NAME controller_type_test COMMAND controller_type_test)
add_executable(psx_stick_test tests/test_psx_stick.cpp src/psx_stick.c)
target_include_directories(psx_stick_test PRIVATE include)
add_test(NAME psx_stick_test COMMAND psx_stick_test)
add_executable(gte_register_access_test
tests/test_gte_register_access.cpp
+5
View File
@@ -204,6 +204,11 @@ int debug_server_get_input_override(void);
* 0 when the injection is buttons-only. */
int debug_server_get_axis_override(unsigned char st[4]);
/* Optional debug-only emulated controller identity set by set_input/press:
* -1 follows normal device selection; otherwise 0=digital, 1=DualShock,
* 2=JogCon. */
int debug_server_get_pad_type_override(void);
/* TCP-controlled turbo mode. When enabled the frontend skips presentation and
* wall-clock pacing at vblank, matching the keyboard TAB turbo path. */
int debug_server_turbo_enabled(void);
+15
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cctype>
#include <cstring>
#include <string>
/* SDL2/3 does not expose a consistently useful wheel class for every mapping.
@@ -23,4 +24,18 @@ inline bool psx_controller_name_is_wheel(const char* name) {
return false;
}
/* Pure reopen policy so unplug and launcher-selected device changes can be
* regression-tested without a physical SDL controller. */
inline bool psx_controller_handle_needs_close(bool controller_selected,
bool handle_open,
bool handle_attached,
const char* selected_guid,
const char* active_guid) {
if (!controller_selected) return true;
if (!handle_open) return false;
if (!handle_attached) return true;
return selected_guid && selected_guid[0] && active_guid && active_guid[0] &&
std::strcmp(selected_guid, active_guid) != 0;
}
#endif
+26 -4
View File
@@ -282,6 +282,9 @@ static int s_input_frames = 0;
* pad sampler alongside the button word. */
static int s_axis_override = 0;
static uint8_t s_axis_st[4] = { 0x80, 0x80, 0x80, 0x80 };
/* Test-only controller identity override for set_input/press. -1 follows the
* selected host device/config; 0/1/2 force digital/DualShock/JogCon. */
static int s_pad_type_override = -1;
/*
* Exact guest-VBlank input route. A client queues run-length encoded digital
@@ -7763,9 +7766,14 @@ static void handle_unwatch(int id, const char *json)
static void handle_set_input(int id, const char *json)
{
char val_str[32];
const int pad_type = json_get_int(json, "pad_type", -1);
if (!json_get_str(json, "buttons", val_str, sizeof(val_str))) {
send_err(id, "missing buttons"); return;
}
if (pad_type < -1 || pad_type > 2) {
send_err(id, "pad_type must be -1 (automatic), 0, 1, or 2"); return;
}
s_pad_type_override = pad_type;
s_input_override = (int)hex_to_u32(val_str);
s_input_frames = 0;
/* Optional stick override: any of lx/ly/rx/ry (0..255) arms it; omitted
@@ -7784,7 +7792,12 @@ static void handle_press(int id, const char *json)
{
int buttons = json_get_int(json, "buttons", -1);
int frames = json_get_int(json, "frames", 2);
const int pad_type = json_get_int(json, "pad_type", -1);
if (buttons < 0) { send_err(id, "missing buttons"); return; }
if (pad_type < -1 || pad_type > 2) {
send_err(id, "pad_type must be -1 (automatic), 0, 1, or 2"); return;
}
s_pad_type_override = pad_type;
s_input_override = buttons;
s_input_frames = frames;
int ax[4] = { json_get_int(json, "lx", -1), json_get_int(json, "ly", -1),
@@ -7813,16 +7826,18 @@ static void handle_pad_status(int id, const char *json)
sio_get_pad_sticks(0, sticks0);
sio_get_pad_sticks(1, sticks1);
send_fmt("{\"id\":%d,\"ok\":true,\"pad\":\"0x%04X\","
"\"slot0\":{\"buttons\":\"0x%04X\",\"connected\":%s,\"analog\":%s,\"sticks\":[%u,%u,%u,%u]},"
"\"slot1\":{\"buttons\":\"0x%04X\",\"connected\":%s,\"analog\":%s,\"sticks\":[%u,%u,%u,%u]},"
"\"override\":%d,\"override_frames\":%d,"
"\"slot0\":{\"buttons\":\"0x%04X\",\"connected\":%s,\"analog\":%s,\"type\":%d,\"sticks\":[%u,%u,%u,%u]},"
"\"slot1\":{\"buttons\":\"0x%04X\",\"connected\":%s,\"analog\":%s,\"type\":%d,\"sticks\":[%u,%u,%u,%u]},"
"\"override\":%d,\"override_frames\":%d,\"override_pad_type\":%d,"
"\"override_axes\":[%u,%u,%u,%u],\"override_axes_valid\":%s}\n",
id, pad0,
pad0, sio_get_pad_connected(0) ? "true" : "false", sio_get_pad_analog(0) ? "true" : "false",
sio_get_pad_analog(0),
sticks0[0], sticks0[1], sticks0[2], sticks0[3],
pad1, sio_get_pad_connected(1) ? "true" : "false", sio_get_pad_analog(1) ? "true" : "false",
sio_get_pad_analog(1),
sticks1[0], sticks1[1], sticks1[2], sticks1[3],
s_input_override, s_input_frames,
s_input_override, s_input_frames, s_pad_type_override,
s_axis_st[0], s_axis_st[1], s_axis_st[2], s_axis_st[3],
s_axis_override ? "true" : "false");
}
@@ -7836,6 +7851,7 @@ static void handle_clear_input(int id, const char *json)
s_input_override = -1;
s_input_frames = 0;
s_axis_override = 0;
s_pad_type_override = -1;
s_axis_st[0] = s_axis_st[1] = s_axis_st[2] = s_axis_st[3] = 0x80;
send_ok(id);
}
@@ -7882,6 +7898,7 @@ static void handle_input_route_start(int id, const char *json)
s_input_override = -1;
s_input_frames = 0;
s_axis_override = 0;
s_pad_type_override = -1;
s_input_route_index = 0;
s_input_route_remaining = s_input_route[0].frames;
s_input_route_active = 1;
@@ -15684,6 +15701,11 @@ int debug_server_get_axis_override(unsigned char st[4])
return 1;
}
int debug_server_get_pad_type_override(void)
{
return s_pad_type_override;
}
int debug_server_turbo_enabled(void)
{
return s_turbo_enabled != 0;
+45 -22
View File
@@ -5038,21 +5038,21 @@ static void refresh_player_devices(void) {
const int netplay = psx_netplay_active();
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
PlayerInput& p = g_players[s];
if (p.kind != 2 ||
(p.handle && !SDL_GameControllerGetAttached(p.handle)))
close_player(p); /* kind change or unplug */
else if (p.handle && p.guid[0]) {
/* The launcher may have assigned a different GUID while the old
* handle is still attached. Release it so open_player can honor
* the new selection and choose the matching emulated type. */
char active_guid[40] = {0};
const bool attached = p.handle &&
SDL_GameControllerGetAttached(p.handle);
if (p.handle) {
SDL_Joystick* joy = SDL_GameControllerGetJoystick(p.handle);
SDL_JoystickGUID active_guid = joy ? SDL_JoystickGetGUID(joy)
: SDL_JoystickGUID{};
char active[40] = {0};
if (joy) SDL_JoystickGetGUIDString(active_guid, active,
(int)sizeof(active));
if (joy && std::strcmp(active, p.guid) != 0) close_player(p);
if (joy) SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(joy),
active_guid,
(int)sizeof(active_guid));
}
/* The launcher may change GUID while the old handle remains attached;
* pure policy covers that and unplug paths with mockable inputs. */
if (psx_controller_handle_needs_close(
p.kind == 2, p.handle != nullptr, attached,
p.guid, active_guid))
close_player(p);
if (p.kind == 2) open_player(p, s);
if (netplay) continue;
const int mode = effective_player_mode_for_sio(p, s);
@@ -5648,8 +5648,13 @@ static void apply_input_override_to_sio(int override_word) {
const int effective_mode = controller_policy_resolve_override_mode(
0, 1, mode, w, st, stick_live, dpad_live);
const int eff_analog =
int eff_analog =
effective_mode == (int)PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
#ifndef PSX_NO_DEBUG_TOOLS
const int debug_type = debug_server_get_pad_type_override();
if (debug_type == SIO_PAD_DUALSHOCK || debug_type == SIO_PAD_JOGCON)
eff_analog = 1;
#endif
/* Injected input only (set_input / dev routing): fold the injected D-pad
* word onto the left stick so stick-only menu/move paths respond to a
* button-bit injection that has no physical stick behind it.
@@ -5669,9 +5674,13 @@ static void apply_input_override_to_sio(int override_word) {
}
if (!eff_analog) { st[0] = st[1] = st[2] = st[3] = 0x80; }
sio_set_pad_sticks(0, st[0], st[1], st[2], st[3]);
const int injected_type = p.steering_wheel ? SIO_PAD_JOGCON
: (eff_analog ? SIO_PAD_DUALSHOCK
: SIO_PAD_DIGITAL);
int injected_type = p.steering_wheel ? SIO_PAD_JOGCON
: (eff_analog ? SIO_PAD_DUALSHOCK
: SIO_PAD_DIGITAL);
#ifndef PSX_NO_DEBUG_TOOLS
if (debug_type >= SIO_PAD_DIGITAL && debug_type <= SIO_PAD_JOGCON)
injected_type = debug_type;
#endif
sio_request_pad_type(0, injected_type);
psx_selfcheck_note_pad(0, w, st[0], st[1], st[2], st[3],
(uint8_t)injected_type);
@@ -5716,8 +5725,13 @@ static int capture_pad_slot(int s, PsxNetPad* out) {
s, player, mode, src, p, policy_buttons, st);
const int eff_analog =
effective_mode == PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
const int frame_type = p.steering_wheel ? SIO_PAD_JOGCON
: (eff_analog ? SIO_PAD_DUALSHOCK : SIO_PAD_DIGITAL);
int frame_type = p.steering_wheel ? SIO_PAD_JOGCON
: (eff_analog ? SIO_PAD_DUALSHOCK : SIO_PAD_DIGITAL);
#ifndef PSX_NO_DEBUG_TOOLS
const int debug_type = debug_server_get_pad_type_override();
if (debug_type >= SIO_PAD_DIGITAL && debug_type <= SIO_PAD_JOGCON)
frame_type = debug_type;
#endif
if (savestate_input_guard_active()) {
out->buttons = 0xFFFFu;
out->lx = out->ly = out->rx = out->ry = 0x80u;
@@ -5937,10 +5951,19 @@ static void capture_override_pad(int override_word, PsxNetPad* out) {
const int effective_mode = controller_policy_resolve_override_mode(
0, 1, mode, w, st, stick_live, dpad_live);
const int eff_analog =
int eff_analog =
effective_mode == (int)PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
const int frame_type = p.steering_wheel ? SIO_PAD_JOGCON
: (eff_analog ? SIO_PAD_DUALSHOCK : SIO_PAD_DIGITAL);
#ifndef PSX_NO_DEBUG_TOOLS
const int debug_type = debug_server_get_pad_type_override();
if (debug_type == SIO_PAD_DUALSHOCK || debug_type == SIO_PAD_JOGCON)
eff_analog = 1;
#endif
int frame_type = p.steering_wheel ? SIO_PAD_JOGCON
: (eff_analog ? SIO_PAD_DUALSHOCK : SIO_PAD_DIGITAL);
#ifndef PSX_NO_DEBUG_TOOLS
if (debug_type >= SIO_PAD_DIGITAL && debug_type <= SIO_PAD_JOGCON)
frame_type = debug_type;
#endif
/* Injected input only; see the note on the sibling fold above. Not
* hardware behaviour, retained solely so injection can steer stick-only
* games. */
+20
View File
@@ -22,6 +22,26 @@ int main() {
return 1;
}
}
struct HotplugCase {
bool selected, open, attached;
const char* selected_guid;
const char* active_guid;
bool close;
} hotplug[] = {
{true, false, false, "wheel-a", nullptr, false},
{true, true, true, "wheel-a", "wheel-a", false},
{true, true, false, "wheel-a", "wheel-a", true},
{true, true, true, "wheel-b", "wheel-a", true},
{false, true, true, "wheel-a", "wheel-a", true},
{true, true, true, nullptr, "wheel-a", false},
};
for (const auto& test : hotplug) {
if (psx_controller_handle_needs_close(test.selected, test.open,
test.attached, test.selected_guid, test.active_guid) != test.close) {
std::fprintf(stderr, "hotplug policy failed\n");
return 1;
}
}
std::puts("controller type classification: passed");
return 0;
}
+12 -1
View File
@@ -87,9 +87,20 @@ int main(void)
netplay_ih_pad_to_frame(&pad, 9, 0, &dig);
CHECK(dig.analog == 0u, "digital frame");
netplay_ih_frame_to_pad(&dig, &pad2);
CHECK(pad2.analog == 0u, "digital roundtrip");
CHECK(pad2.analog == 0u, "digital roundtrip");
}
/* Restore the original DualShock sample for the ring-buffer assertions
* below; the preceding type/steering checks intentionally used later
* ticks and different pad modes. */
memset(&pad, 0, sizeof(pad));
pad.buttons = 0xFDFFu;
pad.lx = (uint8_t)(0x80 + 40);
pad.ly = pad.rx = pad.ry = 0x80;
pad.analog = SIO_PAD_DUALSHOCK;
pad.connected = 1;
netplay_ih_pad_to_frame(&pad, 10, 0, &f);
CHECK(netplay_ih_put(&h, 0, &f), "put local");
CHECK(netplay_ih_get(&h, 0, 10, &got), "get local");
CHECK(got.stick_x == 40, "get stick");
+24
View File
@@ -0,0 +1,24 @@
#include "psx_stick.h"
#include <cstdio>
int main() {
uint8_t x = 0, y = 0;
psx_stick_to_dualshock(0, 0, 4096, 0, &x, &y);
if (x != 0x80 || y != 0x80) return 1;
psx_stick_to_dualshock(4095, 0, 4096, 0, &x, &y);
if (x != 0x80 || y != 0x80) return 2;
psx_stick_to_dualshock(-32768, 0, 4096, 0, &x, &y);
if (x != 0x00 || y != 0x80) return 3;
psx_stick_to_dualshock(32767, 0, 4096, 0, &x, &y);
if (x != 0xFF || y != 0x80) return 4;
psx_stick_to_dualshock(16384, 0, 4096, 0, &x, &y);
if (x <= 0x80 || x >= 0xFF || y != 0x80) return 5;
std::printf("stick mapping: center/left/right/partial pass (partial x=%u)\n",
(unsigned)x);
return 0;
}
+10
View File
@@ -177,6 +177,16 @@ int main(void) {
EXPECT("right.direction", 0x01, xchg(0, 0x00));
(void)xchg(0, 0x00);
/* Partial position stays proportional and reports movement toward center. */
sio_set_pad_sticks(0, 0xC0, 0x80, 0x80, 0x80);
EXPECT("partial.prefix", 0xFF, xchg(0, 0x01));
EXPECT("partial.id", 0xE3, xchg(0, 0x42));
(void)xchg(0, 0x00); (void)xchg(0, 0x00); (void)xchg(0, 0x00);
EXPECT("partial.position", 0x40, xchg(0, 0x00));
EXPECT("partial.sign", 0x00, xchg(0, 0x00));
EXPECT("partial.direction", 0x02, xchg(0, 0x00));
(void)xchg(0, 0x00);
EXPECT("rollback.restore", 1, sio_snapshot_read(snapshot, snapshot_len));
EXPECT("rollback.type", SIO_PAD_JOGCON, sio_get_pad_analog(0));
{