mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
sio: add the NeGcon (0x23) controller type
SIO_PAD_NEGCON = 3 presents a Namco NeGcon (NPC-101): poll ID 0x23 with buttons, twist (left X), then analog I, II and L (Mednafen negcon.cpp). sio_set_pad_negcon() supplies I/II/L per frame like the sticks. The device has no config mode: any command but 0x42 gets the ID byte with no ACK, and game bytes in a poll drive no motors. Multitap seats stay digital unless the analog-on-tap hack is armed; the bulk status block carries the NeGcon frame. Entering or leaving NeGcon is a device swap, not the DualShock analog button: a deferred request applies even while the game holds the 0x44 mode lock, and the swap clears the lock, config latch and motor map. Requests raised mid-config still wait for config exit. 0x44 now selects the analog mode of the device in the port: a JogCon the game set to digital returns as a JogCon instead of becoming a DualShock. The mode lock and that device are guest state, so they join the snapshot as an appended section (older snapshots still load, unlocked). pad_status reports negcon and mode_locked per slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -407,6 +407,19 @@ if(BUILD_TESTING)
|
||||
src/sio.c)
|
||||
target_include_directories(sio_jogcon_protocol_test PRIVATE include)
|
||||
add_test(NAME sio_jogcon_protocol_test COMMAND sio_jogcon_protocol_test)
|
||||
add_executable(sio_negcon_protocol_test
|
||||
tests/test_sio_negcon_protocol.c
|
||||
src/sio.c)
|
||||
target_include_directories(sio_negcon_protocol_test PRIVATE include)
|
||||
add_test(NAME sio_negcon_protocol_test COMMAND sio_negcon_protocol_test)
|
||||
add_executable(sio_negcon_protocol_multitap_test
|
||||
tests/test_sio_negcon_protocol.c
|
||||
src/sio.c)
|
||||
target_include_directories(sio_negcon_protocol_multitap_test PRIVATE include)
|
||||
target_compile_definitions(sio_negcon_protocol_multitap_test PRIVATE
|
||||
PSX_MAX_PLAYERS=5)
|
||||
add_test(NAME sio_negcon_protocol_multitap_test
|
||||
COMMAND sio_negcon_protocol_multitap_test)
|
||||
|
||||
add_executable(controller_type_test tests/test_controller_type.cpp)
|
||||
target_include_directories(controller_type_test PRIVATE include)
|
||||
|
||||
+19
-3
@@ -55,7 +55,12 @@ extern "C" {
|
||||
#define SIO_BASE 0x1F801040
|
||||
|
||||
/* Emulated controller identity used by the game's SIO poll. */
|
||||
enum { SIO_PAD_DIGITAL = 0, SIO_PAD_DUALSHOCK = 1, SIO_PAD_JOGCON = 2 };
|
||||
enum {
|
||||
SIO_PAD_DIGITAL = 0,
|
||||
SIO_PAD_DUALSHOCK = 1,
|
||||
SIO_PAD_JOGCON = 2,
|
||||
SIO_PAD_NEGCON = 3 /* NPC-101: ID 0x23, twist + analog I/II/L, no config */
|
||||
};
|
||||
|
||||
/* Phase 1.0c-v2: cycle-paced SIO model. Default 1 enables the dispatch-
|
||||
* loop quantum tick (gated by g_sio_timing_active). Set to 0 to revert
|
||||
@@ -145,7 +150,8 @@ void sio_set_pad_state_slot(int slot, uint16_t buttons);
|
||||
void sio_set_pad_analog(int slot, int enabled,
|
||||
uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry);
|
||||
/* Set one of SIO_PAD_* explicitly. JogCon consumes left-X as a signed
|
||||
* steering position, centered at 0x80; the remaining axes are ignored. */
|
||||
* steering position, centered at 0x80; NeGcon consumes left-X as its 0..255
|
||||
* twist (0x80 centred). Both ignore the remaining axes. */
|
||||
void sio_set_pad_type(int slot, int type,
|
||||
uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry);
|
||||
|
||||
@@ -156,8 +162,18 @@ void sio_set_pad_type(int slot, int type,
|
||||
* flip can never split a poll or a config handshake. Do NOT call
|
||||
* sio_set_pad_analog every frame for the type — that is for boot/hotplug only. */
|
||||
void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry);
|
||||
/* Request SIO_PAD_* at the next idle, non-config bus boundary. */
|
||||
/* Request SIO_PAD_* at the next idle, non-config bus boundary. A game's 0x44
|
||||
* mode lock holds DualShock/digital/JogCon flips; entering or leaving NeGcon is
|
||||
* a device swap that applies anyway and clears the lock, config latch and
|
||||
* motor map (a different controller in the port). */
|
||||
void sio_request_pad_type(int slot, int type);
|
||||
/* NeGcon analog buttons I, II and L for the next poll (0 released .. 0xFF
|
||||
* fully pressed). Host input like the sticks; ignored unless the slot reports
|
||||
* SIO_PAD_NEGCON. */
|
||||
void sio_set_pad_negcon(int slot, uint8_t i, uint8_t ii, uint8_t l);
|
||||
void sio_get_pad_negcon(int slot, uint8_t out[3]);
|
||||
/* 1 while the game has locked the pad's analog mode (0x44 lock byte 0x03). */
|
||||
int sio_get_pad_mode_locked(int slot);
|
||||
|
||||
/* Connect / disconnect a logical pad (0 .. PSX_MAX_PLAYERS-1). By default no
|
||||
* pads are connected during initial BIOS boot. */
|
||||
|
||||
@@ -7880,26 +7880,34 @@ extern uint16_t sio_get_pad_buttons_slot(int slot);
|
||||
extern int sio_get_pad_connected(int slot);
|
||||
extern int sio_get_pad_analog(int slot);
|
||||
extern void sio_get_pad_sticks(int slot, uint8_t out[4]);
|
||||
extern void sio_get_pad_negcon(int slot, uint8_t out[3]);
|
||||
extern int sio_get_pad_mode_locked(int slot);
|
||||
static void handle_pad_status(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
uint16_t pad0 = sio_get_pad_buttons_slot(0);
|
||||
uint16_t pad1 = sio_get_pad_buttons_slot(1);
|
||||
uint8_t sticks0[4], sticks1[4];
|
||||
uint8_t sticks0[4], sticks1[4], neg0[3], neg1[3];
|
||||
sio_get_pad_sticks(0, sticks0);
|
||||
sio_get_pad_sticks(1, sticks1);
|
||||
sio_get_pad_negcon(0, neg0);
|
||||
sio_get_pad_negcon(1, neg1);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"pad\":\"0x%04X\","
|
||||
"\"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]},"
|
||||
"\"slot0\":{\"buttons\":\"0x%04X\",\"connected\":%s,\"analog\":%s,\"type\":%d,\"sticks\":[%u,%u,%u,%u],"
|
||||
"\"negcon\":[%u,%u,%u],\"mode_locked\":%s},"
|
||||
"\"slot1\":{\"buttons\":\"0x%04X\",\"connected\":%s,\"analog\":%s,\"type\":%d,\"sticks\":[%u,%u,%u,%u],"
|
||||
"\"negcon\":[%u,%u,%u],\"mode_locked\":%s},"
|
||||
"\"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],
|
||||
neg0[0], neg0[1], neg0[2], sio_get_pad_mode_locked(0) ? "true" : "false",
|
||||
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],
|
||||
neg1[0], neg1[1], neg1[2], sio_get_pad_mode_locked(1) ? "true" : "false",
|
||||
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");
|
||||
|
||||
+159
-24
@@ -48,11 +48,24 @@ static void sio_debug_poll_maybe(void) {
|
||||
static uint16_t pad_buttons[PSX_MAX_PLAYERS] = { PSX_PAD_INIT(0xFFFF) };
|
||||
|
||||
/* Per-logical-pad type + analog stick state. Values are SIO_PAD_*; sticks are
|
||||
* 0..255, 0x80 centred. JogCon uses left-X as a signed wheel position. */
|
||||
* 0..255, 0x80 centred. JogCon uses left-X as a signed wheel position; NeGcon
|
||||
* uses it as the 0..255 twist axis (0x80 centred). */
|
||||
static PSX_BSS uint8_t pad_analog[PSX_MAX_PLAYERS];
|
||||
static uint8_t pad_stick[PSX_MAX_PLAYERS][4] = {
|
||||
PSX_PAD_INIT({ 0x80, 0x80, 0x80, 0x80 })
|
||||
}; /* lx,ly,rx,ry */
|
||||
/* NeGcon analog buttons I, II and L (0x00 released .. 0xFF fully pressed).
|
||||
* Host input like the sticks: refreshed before each guest poll, not device
|
||||
* state, so it is not part of the snapshot. */
|
||||
static PSX_BSS uint8_t pad_negcon_analog[PSX_MAX_PLAYERS][3];
|
||||
/* The analog-capable device behind a config-mode pad: SIO_PAD_DUALSHOCK or
|
||||
* SIO_PAD_JOGCON. Command 0x44 switches that device between its analog mode
|
||||
* and digital mode (0x41); remembering the device keeps a JogCon that the game
|
||||
* put in digital mode a JogCon when 0x44 selects analog again, instead of
|
||||
* turning it into a DualShock. Guest-visible, so it is snapshotted. */
|
||||
static uint8_t pad_analog_device[PSX_MAX_PLAYERS] = {
|
||||
PSX_PAD_INIT(SIO_PAD_DUALSHOCK)
|
||||
};
|
||||
|
||||
/* DualShock command 0x4D maps the six writable bytes in a 0x42 poll onto the
|
||||
* two motors: 0x00 = small/high-frequency, 0x01 = large/low-frequency,
|
||||
@@ -252,10 +265,25 @@ static void mtap_finish_42(void) {
|
||||
static uint8_t pad_protocol_id(int logical) {
|
||||
if (pad_in_config[logical]) return 0xF3;
|
||||
if (pad_analog[logical] == SIO_PAD_JOGCON) return 0xE3;
|
||||
if (pad_analog[logical] == SIO_PAD_NEGCON) return 0x23;
|
||||
if (pad_analog[logical] == SIO_PAD_DUALSHOCK) return 0x73;
|
||||
return 0x41;
|
||||
}
|
||||
|
||||
/* NeGcon (NPC-101) poll payload: buttons, twist, then analog I, II and L
|
||||
* (Mednafen psx/input/negcon.cpp). The device has no config mode and no
|
||||
* motors; every byte is fixed by the current host sample. */
|
||||
static void pad_fill_negcon_poll(int logical, uint8_t out[8]) {
|
||||
out[0] = 0x23;
|
||||
out[1] = 0x5A;
|
||||
out[2] = (uint8_t)(pad_buttons[logical] & 0xFF);
|
||||
out[3] = (uint8_t)(pad_buttons[logical] >> 8);
|
||||
out[4] = pad_stick[logical][0];
|
||||
out[5] = pad_negcon_analog[logical][0];
|
||||
out[6] = pad_negcon_analog[logical][1];
|
||||
out[7] = pad_negcon_analog[logical][2];
|
||||
}
|
||||
|
||||
/* Poll payload for the JogCon. Steering is a signed absolute position; the
|
||||
* direction bits report movement since the preceding poll. Byte 7 is reserved. */
|
||||
static void pad_fill_jogcon_poll(int logical, uint8_t id, uint8_t out[8]) {
|
||||
@@ -275,7 +303,8 @@ static void pad_fill_jogcon_poll(int logical, uint8_t id, uint8_t out[8]) {
|
||||
|
||||
/* Fill 8-byte per-pad status block used in multitap bulk 0x42 responses.
|
||||
* Disconnected → all 0xFF. Digital → 0x41, DualShock → 0x73 + axes,
|
||||
* JogCon → 0xE3 + signed steering, config mode → corresponding config ID. */
|
||||
* JogCon → 0xE3 + signed steering, NeGcon → 0x23 + twist/I/II/L,
|
||||
* config mode → corresponding config ID. */
|
||||
static void pad_fill_status8(int logical, uint8_t out[8]) {
|
||||
if (logical < 0 || logical >= PSX_MAX_PLAYERS ||
|
||||
!(pad_connected & (1u << logical))) {
|
||||
@@ -292,6 +321,8 @@ static void pad_fill_status8(int logical, uint8_t out[8]) {
|
||||
uint8_t poll[8];
|
||||
pad_fill_jogcon_poll(logical, id, poll);
|
||||
memcpy(out, poll, sizeof(poll));
|
||||
} else if (pad_analog[logical] == SIO_PAD_NEGCON) {
|
||||
pad_fill_negcon_poll(logical, out);
|
||||
} else if (pad_analog[logical] || pad_in_config[logical]) {
|
||||
out[4] = pad_stick[logical][2]; /* right X */
|
||||
out[5] = pad_stick[logical][3]; /* right Y */
|
||||
@@ -823,7 +854,9 @@ void sio_init(void) {
|
||||
memset(pad_rumble_large, 0, sizeof(pad_rumble_large));
|
||||
memset(pad_jogcon_last_steering, 0, sizeof(pad_jogcon_last_steering));
|
||||
memset(pad_jogcon_motor_command, 0, sizeof(pad_jogcon_motor_command));
|
||||
memset(pad_negcon_analog, 0, sizeof(pad_negcon_analog));
|
||||
for (int i = 0; i < PSX_MAX_PLAYERS; i++) {
|
||||
pad_analog_device[i] = SIO_PAD_DUALSHOCK;
|
||||
pad_buttons[i] = 0xFFFF;
|
||||
pad_analog[i] = 0;
|
||||
pad_stick[i][0] = pad_stick[i][1] = pad_stick[i][2] = pad_stick[i][3] = 0x80;
|
||||
@@ -1003,6 +1036,7 @@ void sio_netplay_canonicalize_session_pads(int slot_count)
|
||||
}
|
||||
pad_in_config[i] = 0;
|
||||
analog_mode_locked[i] = 0;
|
||||
pad_analog_device[i] = SIO_PAD_DUALSHOCK;
|
||||
pad_type_req[i] = -1;
|
||||
memset(pad_rumble_map[i], 0xFF, sizeof(pad_rumble_map[i]));
|
||||
pad_rumble_small[i] = 0;
|
||||
@@ -1061,6 +1095,35 @@ void sio_set_pad_state_slot(int slot, uint16_t buttons) {
|
||||
if (slot >= 0 && slot < PSX_MAX_PLAYERS) pad_buttons[slot] = buttons;
|
||||
}
|
||||
|
||||
/* Entering or leaving NeGcon swaps the controller on the port; it is not the
|
||||
* DualShock analog button. The NeGcon is a pre-DualShock device with no config
|
||||
* mode, mode lock or motors, so the swap clears the config latch, the 0x44 mode
|
||||
* lock and the rumble map/motors exactly as unplugging one pad and plugging in
|
||||
* another would. A game-locked analog mode therefore never pins a DualShock in
|
||||
* place of a requested NeGcon (the lock belongs to the device that left), and a
|
||||
* DualShock that returns powers up unlocked with an unassigned motor map. */
|
||||
static int pad_type_is_device_swap(uint8_t from, uint8_t to) {
|
||||
return (from == SIO_PAD_NEGCON) != (to == SIO_PAD_NEGCON);
|
||||
}
|
||||
|
||||
static void pad_commit_type(int slot, uint8_t type) {
|
||||
const uint8_t from = pad_analog[slot];
|
||||
if (from != type) {
|
||||
pad_jogcon_last_steering[slot] = 0;
|
||||
pad_jogcon_motor_command[slot] = 0;
|
||||
}
|
||||
if (pad_type_is_device_swap(from, type)) {
|
||||
pad_in_config[slot] = 0;
|
||||
analog_mode_locked[slot] = 0;
|
||||
memset(pad_rumble_map[slot], 0xFF, sizeof(pad_rumble_map[slot]));
|
||||
pad_rumble_small[slot] = 0;
|
||||
pad_rumble_large[slot] = 0;
|
||||
}
|
||||
if (type == SIO_PAD_DUALSHOCK || type == SIO_PAD_JOGCON)
|
||||
pad_analog_device[slot] = type;
|
||||
pad_analog[slot] = type;
|
||||
}
|
||||
|
||||
/* Direct set of pad type + sticks. Used at boot/hotplug (refresh_player_devices)
|
||||
* to establish the initial pinned type; safe there because the bus is idle and
|
||||
* no handshake is in flight. Per-frame input must NOT use this for the type —
|
||||
@@ -1073,15 +1136,11 @@ void sio_set_pad_type(int slot, int type,
|
||||
type = SIO_PAD_DIGITAL;
|
||||
lx = ly = rx = ry = 0x80;
|
||||
}
|
||||
if (type < SIO_PAD_DIGITAL || type > SIO_PAD_JOGCON)
|
||||
if (type < SIO_PAD_DIGITAL || type > SIO_PAD_NEGCON)
|
||||
type = SIO_PAD_DIGITAL;
|
||||
if (type == SIO_PAD_JOGCON)
|
||||
pad_supports_config[slot] = 1;
|
||||
if (pad_analog[slot] != (uint8_t)type) {
|
||||
pad_jogcon_last_steering[slot] = 0;
|
||||
pad_jogcon_motor_command[slot] = 0;
|
||||
}
|
||||
pad_analog[slot] = (uint8_t)type;
|
||||
pad_commit_type(slot, (uint8_t)type);
|
||||
pad_type_req[slot] = -1; /* explicit set supersedes any pending request */
|
||||
pad_stick[slot][0] = lx; pad_stick[slot][1] = ly;
|
||||
pad_stick[slot][2] = rx; pad_stick[slot][3] = ry;
|
||||
@@ -1100,13 +1159,33 @@ void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry
|
||||
pad_stick[slot][2] = rx; pad_stick[slot][3] = ry;
|
||||
}
|
||||
|
||||
void sio_set_pad_negcon(int slot, uint8_t i, uint8_t ii, uint8_t l) {
|
||||
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
|
||||
pad_negcon_analog[slot][0] = i;
|
||||
pad_negcon_analog[slot][1] = ii;
|
||||
pad_negcon_analog[slot][2] = l;
|
||||
}
|
||||
|
||||
void sio_get_pad_negcon(int slot, uint8_t out[3]) {
|
||||
if (!out) return;
|
||||
if (slot < 0 || slot >= PSX_MAX_PLAYERS) {
|
||||
out[0] = out[1] = out[2] = 0;
|
||||
return;
|
||||
}
|
||||
memcpy(out, pad_negcon_analog[slot], 3);
|
||||
}
|
||||
|
||||
int sio_get_pad_mode_locked(int slot) {
|
||||
return (slot >= 0 && slot < PSX_MAX_PLAYERS) ? analog_mode_locked[slot] : 0;
|
||||
}
|
||||
|
||||
/* Per-frame type request (the emulated controller type). The flip is
|
||||
* deferred and applied atomically at the next idle, non-config boundary, so it
|
||||
* can never split a poll or a config handshake. A no-op if already that type. */
|
||||
void sio_request_pad_type(int slot, int type) {
|
||||
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
|
||||
if (sio_tap_force_digital(slot)) type = SIO_PAD_DIGITAL;
|
||||
if (type < SIO_PAD_DIGITAL || type > SIO_PAD_JOGCON)
|
||||
if (type < SIO_PAD_DIGITAL || type > SIO_PAD_NEGCON)
|
||||
type = SIO_PAD_DIGITAL;
|
||||
/* A JogCon is a config-capable controller even when its type is selected
|
||||
* through this deferred per-frame path (debug injection / hot selection).
|
||||
@@ -1217,9 +1296,13 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
/* A game-LOCKED analog mode (0x44 ..03) ignores the physical analog
|
||||
* button — and our hybrid auto-flip IS that button — so a locked slot
|
||||
* drops the pending host request instead of applying it. */
|
||||
if (pad_type_req[s] >= 0 && !pad_in_config[s] && !analog_mode_locked[s]) {
|
||||
pad_analog[s] = (uint8_t)pad_type_req[s];
|
||||
* holds the pending host request instead of applying it. A device
|
||||
* swap (into or out of NeGcon) is not that button: it replaces the
|
||||
* locked device and applies regardless (see pad_commit_type). */
|
||||
if (pad_type_req[s] >= 0 && !pad_in_config[s] &&
|
||||
(!analog_mode_locked[s] ||
|
||||
pad_type_is_device_swap(pad_analog[s], (uint8_t)pad_type_req[s]))) {
|
||||
pad_commit_type(s, (uint8_t)pad_type_req[s]);
|
||||
pad_type_req[s] = -1;
|
||||
}
|
||||
}
|
||||
@@ -1308,7 +1391,23 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
* digital-only and just polls. Gate all config branches on this so a
|
||||
* digital-mode pad behaves like real hardware (see pad_supports_config). */
|
||||
const int ds = pad_supports_config[lp];
|
||||
if (tx_byte == 0x42) {
|
||||
if (pad_analog[lp] == SIO_PAD_NEGCON) {
|
||||
/* NeGcon answers only the 0x42 poll. Its ID byte goes out with any
|
||||
* command, but for anything else it does not ACK and the
|
||||
* transaction ends (Mednafen negcon.cpp phase 1). */
|
||||
pad_fill_negcon_poll(lp, pad_response);
|
||||
sio_rx_data = pad_response[0];
|
||||
if (tx_byte == 0x42) {
|
||||
pad_response_len = 8;
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
} else {
|
||||
pad_state = PAD_IDLE;
|
||||
pad_response_len = 0;
|
||||
pad_response_idx = 0;
|
||||
pad_current_cmd = 0;
|
||||
}
|
||||
} else if (tx_byte == 0x42) {
|
||||
/* Read poll. Analog (or in-config) uses the 8-byte format with the
|
||||
* four stick axes; a plain digital pad uses the 4-byte format. */
|
||||
const uint16_t btn = pad_buttons[lp];
|
||||
@@ -1510,7 +1609,7 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
}
|
||||
/* The six data bytes after 0x42's leading 0x00 occupy response
|
||||
* indexes 2..7. Route each through the map negotiated by 0x4D. */
|
||||
if (pad_current_cmd == 0x42 &&
|
||||
if (pad_current_cmd == 0x42 && pad_analog[rs] != SIO_PAD_NEGCON &&
|
||||
pad_response_idx >= 2 && pad_response_idx < 8) {
|
||||
const unsigned map_index = (unsigned)pad_response_idx - 2u;
|
||||
const uint8_t motor = pad_rumble_map[rs][map_index];
|
||||
@@ -1546,12 +1645,11 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
* request so it can't immediately undo the game's choice. */
|
||||
if (!g_pad_legacy_cfg && pad_current_cmd == 0x44 && pad_response_idx == 2 &&
|
||||
pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS) {
|
||||
if (pad_analog[pad_active_logical] == SIO_PAD_JOGCON)
|
||||
pad_analog[pad_active_logical] =
|
||||
(tx_byte == 0x01) ? SIO_PAD_JOGCON : SIO_PAD_DIGITAL;
|
||||
else
|
||||
pad_analog[pad_active_logical] =
|
||||
(tx_byte == 0x01) ? SIO_PAD_DUALSHOCK : SIO_PAD_DIGITAL;
|
||||
/* Analog selects the analog mode of the device in the port (a JogCon
|
||||
* stays a JogCon after a digital round trip), never a new device. */
|
||||
pad_analog[pad_active_logical] =
|
||||
(tx_byte == 0x01) ? pad_analog_device[pad_active_logical]
|
||||
: (uint8_t)SIO_PAD_DIGITAL;
|
||||
pad_type_req[pad_active_logical] = -1;
|
||||
}
|
||||
/* 0x44 lock byte (data position 4, the byte after the mode byte): 0x03 =>
|
||||
@@ -2905,6 +3003,23 @@ static int sio_snap_emit_jogcon(PstW *w) {
|
||||
sizeof(pad_jogcon_motor_command));
|
||||
}
|
||||
|
||||
/* 0x44 mode lock and the analog-capable device behind each port. Both are set
|
||||
* by the guest (0x44) or by a device swap, so rollback/rewind must restore
|
||||
* them; appended after JogCon so older snapshots stay loadable. */
|
||||
static int sio_snap_emit_pad_mode(PstW *w) {
|
||||
return pst_w_bytes(w, analog_mode_locked, sizeof(analog_mode_locked)) &&
|
||||
pst_w_bytes(w, pad_analog_device, sizeof(pad_analog_device));
|
||||
}
|
||||
|
||||
static void sio_pad_mode_legacy_defaults(void) {
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
analog_mode_locked[s] = 0;
|
||||
pad_analog_device[s] = pad_analog[s] == SIO_PAD_JOGCON
|
||||
? (uint8_t)SIO_PAD_JOGCON
|
||||
: (uint8_t)SIO_PAD_DUALSHOCK;
|
||||
}
|
||||
}
|
||||
|
||||
static int sio_snap_emit_fsm(PstW *w) {
|
||||
return sio_snap_emit_fsm_pace(w) && sio_snap_emit_fsm_meta(w);
|
||||
}
|
||||
@@ -2912,12 +3027,12 @@ static int sio_snap_emit_fsm(PstW *w) {
|
||||
static int sio_snap_emit(PstW *w) {
|
||||
return sio_snap_emit_regs(w) && sio_snap_emit_pads(w) &&
|
||||
sio_snap_emit_mc(w) && sio_snap_emit_fsm(w) && sio_snap_emit_rumble(w) &&
|
||||
sio_snap_emit_jogcon(w);
|
||||
sio_snap_emit_jogcon(w) && sio_snap_emit_pad_mode(w);
|
||||
}
|
||||
|
||||
/* Cumulative section end offsets in the snapshot wire:
|
||||
* out[0]=regs, out[1]=pads, out[2]=memcard, out[3]=fsm_pace (netplay),
|
||||
* out[4]=full including fsm metadata, rumble, and JogCon state. */
|
||||
* out[4]=full including fsm metadata, rumble, JogCon and pad-mode state. */
|
||||
void sio_snapshot_section_ends(uint32_t out[5]) {
|
||||
PstW w;
|
||||
pst_w_init(&w, NULL, 0);
|
||||
@@ -2932,6 +3047,7 @@ void sio_snapshot_section_ends(uint32_t out[5]) {
|
||||
(void)sio_snap_emit_fsm_meta(&w);
|
||||
(void)sio_snap_emit_rumble(&w);
|
||||
(void)sio_snap_emit_jogcon(&w);
|
||||
(void)sio_snap_emit_pad_mode(&w);
|
||||
out[4] = (uint32_t)w.written;
|
||||
}
|
||||
|
||||
@@ -3027,6 +3143,7 @@ static int sio_snap_parse(PstR *r) {
|
||||
pad_rumble_map[s][0] = 0x00;
|
||||
pad_rumble_map[s][1] = 0x01;
|
||||
}
|
||||
sio_pad_mode_legacy_defaults();
|
||||
return 1;
|
||||
}
|
||||
if (!pst_r_bytes(r, pad_rumble_map, sizeof(pad_rumble_map)) ||
|
||||
@@ -3038,6 +3155,7 @@ static int sio_snap_parse(PstR *r) {
|
||||
if (r->p == r->end) {
|
||||
memset(pad_jogcon_last_steering, 0, sizeof(pad_jogcon_last_steering));
|
||||
memset(pad_jogcon_motor_command, 0, sizeof(pad_jogcon_motor_command));
|
||||
sio_pad_mode_legacy_defaults();
|
||||
return 1;
|
||||
}
|
||||
if (!pst_r_bytes(r, pad_jogcon_last_steering,
|
||||
@@ -3045,6 +3163,19 @@ static int sio_snap_parse(PstR *r) {
|
||||
!pst_r_bytes(r, pad_jogcon_motor_command,
|
||||
sizeof(pad_jogcon_motor_command)))
|
||||
return 0;
|
||||
/* Pre-pad-mode snapshots: unlocked, device inferred from the type. */
|
||||
if (r->p == r->end) {
|
||||
sio_pad_mode_legacy_defaults();
|
||||
return 1;
|
||||
}
|
||||
if (!pst_r_bytes(r, analog_mode_locked, sizeof(analog_mode_locked)) ||
|
||||
!pst_r_bytes(r, pad_analog_device, sizeof(pad_analog_device)))
|
||||
return 0;
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
analog_mode_locked[s] = analog_mode_locked[s] ? 1 : 0;
|
||||
if (pad_analog_device[s] != SIO_PAD_JOGCON)
|
||||
pad_analog_device[s] = SIO_PAD_DUALSHOCK;
|
||||
}
|
||||
if (r->p != r->end) return 0;
|
||||
return 1;
|
||||
}
|
||||
@@ -3073,9 +3204,13 @@ int sio_snapshot_validate(const uint8_t *p, uint32_t len) {
|
||||
sizeof(pad_rumble_large));
|
||||
const uint32_t jogcon_bytes = (uint32_t)(sizeof(pad_jogcon_last_steering) +
|
||||
sizeof(pad_jogcon_motor_command));
|
||||
const uint32_t mode_bytes = (uint32_t)(sizeof(analog_mode_locked) +
|
||||
sizeof(pad_analog_device));
|
||||
return p && (len == current ||
|
||||
(len < current && len + jogcon_bytes == current) ||
|
||||
(len < current && len + jogcon_bytes + rumble_bytes == current));
|
||||
(len < current && len + mode_bytes == current) ||
|
||||
(len < current && len + mode_bytes + jogcon_bytes == current) ||
|
||||
(len < current &&
|
||||
len + mode_bytes + jogcon_bytes + rumble_bytes == current));
|
||||
}
|
||||
|
||||
int sio_snapshot_read(const uint8_t *p, uint32_t len) {
|
||||
|
||||
@@ -170,16 +170,19 @@ int main(void) {
|
||||
EXPECT("snapshot.current.small", 0x01, small);
|
||||
EXPECT("snapshot.current.large", 0x80, large);
|
||||
|
||||
/* JogCon rollback fields add four bytes for a two-seat build. Removing
|
||||
* those restores the preceding rumble-era format; removing all twenty
|
||||
* trailing bytes models a pre-rumble state. */
|
||||
EXPECT("snapshot.pre-jogcon.read", 1,
|
||||
/* The pad-mode section (0x44 lock + device) and the JogCon rollback
|
||||
* fields each add four bytes for a two-seat build. Removing them restores
|
||||
* the preceding formats; removing all twenty-four trailing bytes models a
|
||||
* pre-rumble state. */
|
||||
EXPECT("snapshot.pre-pad-mode.read", 1,
|
||||
sio_snapshot_read(snapshot, snapshot_len - 4));
|
||||
EXPECT("snapshot.pre-jogcon.read", 1,
|
||||
sio_snapshot_read(snapshot, snapshot_len - 8));
|
||||
EXPECT("snapshot.pre-jogcon.small", 0x01,
|
||||
(sio_get_pad_rumble(0, &small, &large), small));
|
||||
EXPECT("snapshot.pre-jogcon.large", 0x80, large);
|
||||
EXPECT("snapshot.legacy.read", 1,
|
||||
sio_snapshot_read(snapshot, snapshot_len - 20));
|
||||
sio_snapshot_read(snapshot, snapshot_len - 24));
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("snapshot.legacy.small", 0x00, small);
|
||||
EXPECT("snapshot.legacy.large", 0x00, large);
|
||||
|
||||
@@ -221,10 +221,14 @@ int main(void) {
|
||||
(void)xchg(0, 0x00);
|
||||
|
||||
const uint32_t snapshot_len = sio_snapshot_bytes();
|
||||
/* JogCon steering[0] sits before steering[1..], the motor bytes and the
|
||||
* trailing pad-mode section (lock + device per pad). */
|
||||
const uint32_t steering_at = snapshot_len - 2u * PSX_MAX_PLAYERS -
|
||||
2u * PSX_MAX_PLAYERS;
|
||||
uint8_t *snapshot = (uint8_t *)malloc(snapshot_len);
|
||||
if (!snapshot) return 1;
|
||||
sio_snapshot_write(snapshot);
|
||||
EXPECT("snapshot.steering.field", 0x80, snapshot[snapshot_len - 4]);
|
||||
EXPECT("snapshot.steering.field", 0x80, snapshot[steering_at]);
|
||||
|
||||
sio_set_pad_sticks(0, 0xFF, 0x80, 0x80, 0x80);
|
||||
EXPECT("right.prefix", 0xFF, xchg(0, 0x01));
|
||||
@@ -251,8 +255,8 @@ int main(void) {
|
||||
uint8_t *roundtrip = (uint8_t *)malloc(snapshot_len);
|
||||
if (roundtrip) {
|
||||
sio_snapshot_write(roundtrip);
|
||||
EXPECT("rollback.steering.field", snapshot[snapshot_len - 4],
|
||||
roundtrip[snapshot_len - 4]);
|
||||
EXPECT("rollback.steering.field", snapshot[steering_at],
|
||||
roundtrip[steering_at]);
|
||||
free(roundtrip);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
/* NeGcon wire protocol regression: ID 0x23 poll layout (twist, I, II, L),
|
||||
* no config mode, device-swap edges against the 0x44 mode lock, deferred type
|
||||
* requests during config, 0x44 keeping the device, snapshot of the pad mode,
|
||||
* and multitap force-digital (built a second time with PSX_MAX_PLAYERS=5). */
|
||||
|
||||
#include "sio.h"
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
uint32_t sio_snapshot_bytes(void);
|
||||
void sio_snapshot_write(uint8_t *p);
|
||||
int sio_snapshot_read(const uint8_t *p, uint32_t len);
|
||||
|
||||
uint32_t i_stat = 0;
|
||||
uint32_t i_mask = 0;
|
||||
uint32_t g_debug_current_func_addr = 0;
|
||||
uint32_t g_debug_last_store_pc = 0;
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; }
|
||||
uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; }
|
||||
|
||||
/* sio.c gates card-transfer deferral on `psx_get_cycle_count() < deadline`, so
|
||||
* a constant clock would defer forever. Advance monotonically. */
|
||||
uint64_t psx_get_cycle_count(void) {
|
||||
static uint64_t t;
|
||||
return t += 64;
|
||||
}
|
||||
uint32_t memory_get_sr(void) { return 0; }
|
||||
void debug_server_poll(void) {}
|
||||
void debug_server_log_sio_write(uint32_t a, uint32_t v, uint8_t w) {
|
||||
(void)a; (void)v; (void)w;
|
||||
}
|
||||
void starvation_ring_record(uint8_t k, uint8_t tx, uint8_t rx, uint16_t c,
|
||||
uint16_t st, uint8_t d, uint8_t ss, uint16_t ms,
|
||||
uint8_t mc, uint16_t sec, uint8_t di, uint32_t f) {
|
||||
(void)k; (void)tx; (void)rx; (void)c; (void)st; (void)d; (void)ss;
|
||||
(void)ms; (void)mc; (void)sec; (void)di; (void)f;
|
||||
}
|
||||
void card_read_summary_record(uint8_t s, uint8_t c, uint16_t sec, uint8_t e,
|
||||
uint8_t chk, uint8_t d0, uint8_t d1, uint32_t f) {
|
||||
(void)s; (void)c; (void)sec; (void)e; (void)chk; (void)d0; (void)d1; (void)f;
|
||||
}
|
||||
void card_data_writes_arm(uint8_t v, uint16_t s, uint8_t i, uint8_t sl) {
|
||||
(void)v; (void)s; (void)i; (void)sl;
|
||||
}
|
||||
void event_ring_record_aux(uint16_t k, uint8_t d, uint32_t a) {
|
||||
(void)k; (void)d; (void)a;
|
||||
}
|
||||
void psx_irq_raise(uint32_t bit, uint32_t detail) {
|
||||
(void)detail; i_stat |= 1u << bit;
|
||||
}
|
||||
int memcard_is_present(int card) { (void)card; return 0; }
|
||||
int memcard_read_sector(int card, int sector, uint8_t *buf) {
|
||||
(void)card; (void)sector; memset(buf, 0, 128); return -1;
|
||||
}
|
||||
int memcard_write_sector(int card, int sector, const uint8_t *buf) {
|
||||
(void)card; (void)sector; (void)buf; return -1;
|
||||
}
|
||||
void memcard_flush(int card) { (void)card; }
|
||||
|
||||
#define SIO_TX_DATA 0x1F801040u
|
||||
#define SIO_RX_DATA 0x1F801040u
|
||||
#define SIO_CTRL 0x1F80104Au
|
||||
#define CTRL_TX_EN (1u << 0)
|
||||
#define CTRL_SELECT (1u << 1)
|
||||
#define CTRL_ACK (1u << 4)
|
||||
#define CTRL_ACK_IRQ_EN (1u << 12)
|
||||
#define CTRL_SLOT (1u << 13)
|
||||
|
||||
static int failures;
|
||||
|
||||
#define EXPECT(label, expected, actual) do { \
|
||||
unsigned e_ = (unsigned)(expected), a_ = (unsigned)(actual); \
|
||||
if (e_ != a_) { \
|
||||
fprintf(stderr, "FAIL %s: expected=0x%X actual=0x%X\n", \
|
||||
label, e_, a_); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static int last_ack;
|
||||
|
||||
/* One byte on the pad bus; last_ack reports whether the device pulsed ACK
|
||||
* (IRQ7) after it, i.e. whether it wants another byte. */
|
||||
static uint8_t xchg(int slot, uint8_t tx) {
|
||||
const uint16_t ctrl = (uint16_t)(CTRL_TX_EN | CTRL_SELECT |
|
||||
CTRL_ACK_IRQ_EN | (slot ? CTRL_SLOT : 0));
|
||||
i_stat = 0;
|
||||
sio_write(SIO_CTRL, ctrl);
|
||||
sio_write(SIO_TX_DATA, tx);
|
||||
sio_tick(2000);
|
||||
const uint8_t rx = (uint8_t)sio_read(SIO_RX_DATA);
|
||||
last_ack = (i_stat & (1u << 7)) ? 1 : 0;
|
||||
sio_write(SIO_CTRL, ctrl | CTRL_ACK);
|
||||
return rx;
|
||||
}
|
||||
|
||||
/* Full 0x42 poll; returns the ID and fills the six bytes after 0x5A. */
|
||||
static uint8_t poll(int slot, uint8_t out[6]) {
|
||||
uint8_t id;
|
||||
(void)xchg(slot, 0x01);
|
||||
id = xchg(slot, 0x42);
|
||||
EXPECT("poll.status", 0x5A, xchg(slot, 0x00));
|
||||
for (int i = 0; i < 6; i++) out[i] = xchg(slot, 0x00);
|
||||
return id;
|
||||
}
|
||||
|
||||
/* Short 0x42 poll for a 0x41 pad (two data bytes). */
|
||||
static uint8_t poll_id(int slot) {
|
||||
uint8_t d[6];
|
||||
uint8_t id;
|
||||
(void)xchg(slot, 0x01);
|
||||
id = xchg(slot, 0x42);
|
||||
if (id == 0x41) {
|
||||
(void)xchg(slot, 0x00); (void)xchg(slot, 0x00); (void)xchg(slot, 0x00);
|
||||
return id;
|
||||
}
|
||||
(void)xchg(slot, 0x00);
|
||||
for (int i = 0; i < 6; i++) d[i] = xchg(slot, 0x00);
|
||||
(void)d;
|
||||
return id;
|
||||
}
|
||||
|
||||
/* 8-byte config command (0x43/0x44/0x4D...) with explicit data bytes. */
|
||||
static uint8_t config_cmd(int slot, uint8_t cmd, const uint8_t data[6],
|
||||
uint8_t echo[6]) {
|
||||
uint8_t id;
|
||||
(void)xchg(slot, 0x01);
|
||||
id = xchg(slot, cmd);
|
||||
(void)xchg(slot, 0x00);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t r = xchg(slot, data[i]);
|
||||
if (echo) echo[i] = r;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
static const uint8_t k_enter[6] = { 0x01, 0, 0, 0, 0, 0 };
|
||||
static const uint8_t k_exit[6] = { 0x00, 0, 0, 0, 0, 0 };
|
||||
static const uint8_t k_lock_analog[6] = { 0x01, 0x03, 0, 0, 0, 0 };
|
||||
static const uint8_t k_digital[6] = { 0x00, 0x02, 0, 0, 0, 0 };
|
||||
static const uint8_t k_analog[6] = { 0x01, 0x02, 0, 0, 0, 0 };
|
||||
static const uint8_t k_map[6] = { 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
|
||||
static void negcon_wire(void) {
|
||||
uint8_t d[6];
|
||||
sio_init();
|
||||
sio_connect_pad(0);
|
||||
sio_set_pad_type(0, SIO_PAD_NEGCON, 0x80, 0x80, 0x80, 0x80);
|
||||
sio_set_pad_state_slot(0, 0xBFEFu); /* Up + Cross held */
|
||||
sio_set_pad_sticks(0, 0x30, 0x11, 0x22, 0x33); /* twist = left X */
|
||||
sio_set_pad_negcon(0, 0xC0, 0x40, 0x07);
|
||||
EXPECT("wire.prefix", 0xFF, xchg(0, 0x01));
|
||||
EXPECT("wire.prefix.ack", 1, last_ack);
|
||||
EXPECT("wire.id", 0x23, xchg(0, 0x42));
|
||||
EXPECT("wire.id.ack", 1, last_ack);
|
||||
EXPECT("wire.status", 0x5A, xchg(0, 0x00));
|
||||
EXPECT("wire.buttons.low", 0xEF, xchg(0, 0x00));
|
||||
EXPECT("wire.buttons.high", 0xBF, xchg(0, 0x00));
|
||||
EXPECT("wire.twist", 0x30, xchg(0, 0x00));
|
||||
EXPECT("wire.I", 0xC0, xchg(0, 0x00));
|
||||
EXPECT("wire.II", 0x40, xchg(0, 0x00));
|
||||
EXPECT("wire.L.ack", 1, last_ack);
|
||||
EXPECT("wire.L", 0x07, xchg(0, 0x00));
|
||||
EXPECT("wire.end.noack", 0, last_ack);
|
||||
|
||||
/* Not config-capable: any other command gets the ID byte with no ACK and
|
||||
* the transaction ends; 0x43 never latches config mode. */
|
||||
EXPECT("cfg.prefix", 0xFF, xchg(0, 0x01));
|
||||
EXPECT("cfg.id", 0x23, xchg(0, 0x43));
|
||||
EXPECT("cfg.noack", 0, last_ack);
|
||||
EXPECT("cfg.hiz", 0xFF, xchg(0, 0x00));
|
||||
EXPECT("cfg.after.id", 0x23, poll(0, d));
|
||||
EXPECT("cfg.after.I", 0xC0, d[3]);
|
||||
|
||||
/* Pressure is host input: the next poll reports the new sample. */
|
||||
sio_set_pad_negcon(0, 0x00, 0xFF, 0x00);
|
||||
EXPECT("resample.id", 0x23, poll(0, d));
|
||||
EXPECT("resample.I", 0x00, d[3]);
|
||||
EXPECT("resample.II", 0xFF, d[4]);
|
||||
{
|
||||
uint8_t back[3];
|
||||
sio_get_pad_negcon(0, back);
|
||||
EXPECT("getter.II", 0xFF, back[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/* A game-locked DualShock holds analog-button flips but not a NeGcon swap;
|
||||
* the swap clears the lock and the motor map, and the DualShock that comes
|
||||
* back powers up unlocked. */
|
||||
static void lock_and_swap(void) {
|
||||
uint8_t d[6], echo[6];
|
||||
sio_init();
|
||||
sio_connect_pad(0);
|
||||
sio_set_pad_type(0, SIO_PAD_DUALSHOCK, 0x80, 0x80, 0x80, 0x80);
|
||||
sio_set_pad_config_capable(0, 1);
|
||||
EXPECT("lock.enter.id", 0x73, config_cmd(0, 0x43, k_enter, NULL));
|
||||
EXPECT("lock.mode.id", 0xF3, config_cmd(0, 0x44, k_lock_analog, NULL));
|
||||
EXPECT("lock.map.id", 0xF3, config_cmd(0, 0x4D, k_map, NULL));
|
||||
EXPECT("lock.exit.id", 0xF3, config_cmd(0, 0x43, k_exit, NULL));
|
||||
EXPECT("lock.locked", 1, sio_get_pad_mode_locked(0));
|
||||
|
||||
sio_request_pad_type(0, SIO_PAD_DIGITAL);
|
||||
EXPECT("lock.holds.digital", 0x73, poll(0, d));
|
||||
EXPECT("lock.holds.type", SIO_PAD_DUALSHOCK, sio_get_pad_analog(0));
|
||||
|
||||
sio_set_pad_negcon(0, 0x99, 0, 0);
|
||||
sio_request_pad_type(0, SIO_PAD_NEGCON);
|
||||
EXPECT("swap.in.id", 0x23, poll(0, d));
|
||||
EXPECT("swap.in.I", 0x99, d[3]);
|
||||
EXPECT("swap.in.unlocked", 0, sio_get_pad_mode_locked(0));
|
||||
/* A NeGcon has no motors: game bytes in a poll drive nothing. */
|
||||
{
|
||||
uint8_t small = 1, large = 1;
|
||||
(void)xchg(0, 0x01); (void)xchg(0, 0x42); (void)xchg(0, 0x00);
|
||||
(void)xchg(0, 0xFF); (void)xchg(0, 0xFF);
|
||||
for (int i = 0; i < 4; i++) (void)xchg(0, 0x00);
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("swap.in.small", 0, small);
|
||||
EXPECT("swap.in.large", 0, large);
|
||||
}
|
||||
|
||||
sio_request_pad_type(0, SIO_PAD_DUALSHOCK);
|
||||
EXPECT("swap.out.id", 0x73, poll(0, d));
|
||||
sio_request_pad_type(0, SIO_PAD_DIGITAL);
|
||||
EXPECT("swap.out.unlocked.flip", 0x41, poll_id(0));
|
||||
sio_request_pad_type(0, SIO_PAD_DUALSHOCK);
|
||||
EXPECT("swap.out.back", 0x73, poll(0, d));
|
||||
EXPECT("swap.out.enter", 0x73, config_cmd(0, 0x43, k_enter, NULL));
|
||||
EXPECT("swap.out.map.id", 0xF3, config_cmd(0, 0x4D, k_map, echo));
|
||||
for (int i = 0; i < 6; i++)
|
||||
EXPECT("swap.out.map.unassigned", 0xFF, echo[i]);
|
||||
(void)config_cmd(0, 0x43, k_exit, NULL);
|
||||
}
|
||||
|
||||
/* A type request raised during a config handshake waits for config exit. */
|
||||
static void deferred_during_config(void) {
|
||||
uint8_t d[6];
|
||||
sio_init();
|
||||
sio_connect_pad(0);
|
||||
sio_set_pad_type(0, SIO_PAD_DUALSHOCK, 0x80, 0x80, 0x80, 0x80);
|
||||
EXPECT("defer.enter", 0x73, config_cmd(0, 0x43, k_enter, NULL));
|
||||
sio_request_pad_type(0, SIO_PAD_NEGCON);
|
||||
EXPECT("defer.in.config", 0xF3, poll(0, d));
|
||||
EXPECT("defer.type.held", SIO_PAD_DUALSHOCK, sio_get_pad_analog(0));
|
||||
EXPECT("defer.exit", 0xF3, config_cmd(0, 0x43, k_exit, NULL));
|
||||
EXPECT("defer.applied", 0x23, poll(0, d));
|
||||
}
|
||||
|
||||
/* 0x44 selects the analog mode of the device in the port: a JogCon that the
|
||||
* game put in digital mode returns as a JogCon, not a DualShock. */
|
||||
static void set_mode_keeps_device(void) {
|
||||
uint8_t d[6];
|
||||
sio_init();
|
||||
sio_connect_pad(0);
|
||||
sio_set_pad_type(0, SIO_PAD_JOGCON, 0x80, 0x80, 0x80, 0x80);
|
||||
EXPECT("0x44.enter", 0xE3, config_cmd(0, 0x43, k_enter, NULL));
|
||||
(void)config_cmd(0, 0x44, k_digital, NULL);
|
||||
EXPECT("0x44.digital", SIO_PAD_DIGITAL, sio_get_pad_analog(0));
|
||||
(void)config_cmd(0, 0x44, k_analog, NULL);
|
||||
EXPECT("0x44.jogcon", SIO_PAD_JOGCON, sio_get_pad_analog(0));
|
||||
(void)config_cmd(0, 0x43, k_exit, NULL);
|
||||
EXPECT("0x44.poll", 0xE3, poll(0, d));
|
||||
|
||||
sio_set_pad_type(0, SIO_PAD_DUALSHOCK, 0x80, 0x80, 0x80, 0x80);
|
||||
EXPECT("0x44.ds.enter", 0x73, config_cmd(0, 0x43, k_enter, NULL));
|
||||
(void)config_cmd(0, 0x44, k_digital, NULL);
|
||||
(void)config_cmd(0, 0x44, k_analog, NULL);
|
||||
EXPECT("0x44.dualshock", SIO_PAD_DUALSHOCK, sio_get_pad_analog(0));
|
||||
(void)config_cmd(0, 0x43, k_exit, NULL);
|
||||
}
|
||||
|
||||
/* Lock and device survive rollback; a snapshot from before the pad-mode
|
||||
* section still loads, unlocked. */
|
||||
static void snapshot_pad_mode(void) {
|
||||
uint8_t d[6];
|
||||
const uint32_t mode_bytes = 2u * PSX_MAX_PLAYERS;
|
||||
sio_init();
|
||||
sio_connect_pad(0);
|
||||
sio_set_pad_type(0, SIO_PAD_JOGCON, 0x80, 0x80, 0x80, 0x80);
|
||||
(void)config_cmd(0, 0x43, k_enter, NULL);
|
||||
(void)config_cmd(0, 0x44, k_lock_analog, NULL);
|
||||
(void)config_cmd(0, 0x43, k_exit, NULL);
|
||||
EXPECT("snap.locked", 1, sio_get_pad_mode_locked(0));
|
||||
|
||||
const uint32_t len = sio_snapshot_bytes();
|
||||
uint8_t *snap = (uint8_t *)malloc(len);
|
||||
if (!snap) { failures++; return; }
|
||||
sio_snapshot_write(snap);
|
||||
|
||||
sio_request_pad_type(0, SIO_PAD_NEGCON);
|
||||
EXPECT("snap.swap", 0x23, poll(0, d));
|
||||
EXPECT("snap.swap.unlocked", 0, sio_get_pad_mode_locked(0));
|
||||
|
||||
EXPECT("snap.restore", 1, sio_snapshot_read(snap, len));
|
||||
EXPECT("snap.restore.type", SIO_PAD_JOGCON, sio_get_pad_analog(0));
|
||||
EXPECT("snap.restore.locked", 1, sio_get_pad_mode_locked(0));
|
||||
sio_request_pad_type(0, SIO_PAD_DIGITAL);
|
||||
EXPECT("snap.restore.lock.holds", 0xE3, poll(0, d));
|
||||
|
||||
EXPECT("snap.legacy.restore", 1, sio_snapshot_read(snap, len - mode_bytes));
|
||||
EXPECT("snap.legacy.unlocked", 0, sio_get_pad_mode_locked(0));
|
||||
EXPECT("snap.bad.len", 0, sio_snapshot_read(snap, len - 1));
|
||||
free(snap);
|
||||
}
|
||||
|
||||
#if PSX_MAX_PLAYERS >= 5
|
||||
/* Tap seats are digital unless the DualShock-on-tap hack is armed; with it,
|
||||
* the bulk status block carries the NeGcon payload. */
|
||||
static void multitap(void) {
|
||||
uint8_t r[34];
|
||||
sio_init();
|
||||
sio_set_multitap(1);
|
||||
sio_set_multitap_port(0);
|
||||
sio_set_multitap_analog(0);
|
||||
sio_connect_pad(0);
|
||||
sio_set_pad_negcon(0, 0x10, 0x20, 0x30);
|
||||
sio_set_pad_sticks(0, 0x44, 0x80, 0x80, 0x80);
|
||||
sio_request_pad_type(0, SIO_PAD_NEGCON);
|
||||
(void)xchg(0, 0x01);
|
||||
EXPECT("tap.forced.digital.id", 0x41, xchg(0, 0x42));
|
||||
(void)xchg(0, 0x00); (void)xchg(0, 0x00); (void)xchg(0, 0x00);
|
||||
EXPECT("tap.forced.digital", SIO_PAD_DIGITAL, sio_get_pad_analog(0));
|
||||
|
||||
sio_set_multitap_analog(1);
|
||||
sio_request_pad_type(0, SIO_PAD_NEGCON);
|
||||
/* Slot A poll with TAP=1 applies the request and arms the bulk read. */
|
||||
(void)xchg(0, 0x01);
|
||||
EXPECT("tap.hack.slot_a.id", 0x23, xchg(0, 0x42));
|
||||
EXPECT("tap.hack.negcon", SIO_PAD_NEGCON, sio_get_pad_analog(0));
|
||||
(void)xchg(0, 0x01);
|
||||
for (int i = 0; i < 6; i++) (void)xchg(0, 0x00);
|
||||
(void)xchg(0, 0x01);
|
||||
r[0] = xchg(0, 0x42);
|
||||
for (int i = 1; i < 34; i++) r[i] = xchg(0, 0x00);
|
||||
EXPECT("tap.bulk.id", 0x80, r[0]);
|
||||
EXPECT("tap.bulk.a.id", 0x23, r[2]);
|
||||
EXPECT("tap.bulk.a.twist", 0x44, r[6]);
|
||||
EXPECT("tap.bulk.a.I", 0x10, r[7]);
|
||||
EXPECT("tap.bulk.a.II", 0x20, r[8]);
|
||||
EXPECT("tap.bulk.a.L", 0x30, r[9]);
|
||||
sio_set_multitap(0);
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(void) {
|
||||
negcon_wire();
|
||||
lock_and_swap();
|
||||
deferred_during_config();
|
||||
set_mode_keeps_device();
|
||||
snapshot_pad_mode();
|
||||
#if PSX_MAX_PLAYERS >= 5
|
||||
multitap();
|
||||
#endif
|
||||
if (failures) {
|
||||
fprintf(stderr, "NeGcon SIO protocol (players=%d): %d failure(s)\n",
|
||||
PSX_MAX_PLAYERS, failures);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "NeGcon SIO protocol (players=%d): passed\n", PSX_MAX_PLAYERS);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user