mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
feat(input): restore DualShock rumble output
This commit is contained in:
@@ -150,14 +150,13 @@ will still see our HYBRID auto-flip yank it back to digital — exactly the kind
|
||||
of "type flips underneath the game" desync the deferred-request machinery was
|
||||
built to avoid but cannot prevent, because we never learned the pad is locked.
|
||||
|
||||
### D7 — **`0x4D` rumble-map is a canned constant, not an echo-back map** (semantic)
|
||||
Beetle's `0x4D` echoes the previous `rumble_magic[]` while latching the new
|
||||
mapping bytes (`dualshock.cpp:993-1024`); the map then routes `0x42` weak/strong
|
||||
rumble params (`dualshock.cpp:617-644`). Ours returns a fixed
|
||||
`{0xF3,0x5A,0,0,0,0,0,0}` (`sio.c:812`/`830`) and implements **no rumble at
|
||||
all.** Acceptable (no rumble output device), but a game that *reads back* the
|
||||
`0x4D` map to confirm rumble init will get the wrong echo and may decide rumble
|
||||
is absent. Low player-visible impact (no rumble hardware anyway).
|
||||
### D7 — **`0x4D` rumble-map negotiation and host output** (resolved)
|
||||
Resolved: `0x4D` now echoes the previous six-byte motor map while latching the
|
||||
new map. Mapped motor bytes from later `0x42` polls are retained per slot and
|
||||
forwarded to SDL's low/high-frequency rumble channels by the frontend. The
|
||||
protocol path is pinned by `sio_dualshock_rumble_test`; legacy savestates remain
|
||||
loadable and receive the conventional `{0x00,0x01,0xFF,...}` map with stopped
|
||||
motors.
|
||||
|
||||
### D8 — **`0x45` analog-status byte is hard-coded `0x03` (analog on)** (semantic)
|
||||
Beetle `0x4501` reports `transmit_buffer[1] = analog_mode ? 0x01 : 0x00`
|
||||
@@ -289,9 +288,8 @@ every title — which is exactly what the LEGACY comment predicted, letting the
|
||||
The flag, legacy SIO branches, config key, debug command, helper script, and
|
||||
Tomba opt-in were removed. All titles use the modern DualShock state machine.
|
||||
|
||||
5. **[P2] Echo-back `0x4D` rumble map** (D7). Even with no rumble output,
|
||||
echo the previous `rumble_magic[]` bytes (`dualshock.cpp:993-1024`) so a game
|
||||
that confirms rumble-init reads sane values. Low visible impact.
|
||||
5. **[DONE 2026-08-06] Echo-back `0x4D` rumble map and forward motors.** The
|
||||
SIO model now routes negotiated weak/strong values to the SDL frontend.
|
||||
|
||||
6. **[P2] Move pad ACK/IRQ timing onto the cycle-paced shifter** (D1). Remove
|
||||
the pad fast-path bypass (`sio.c:1386`) so pad bytes use
|
||||
|
||||
@@ -52,6 +52,12 @@ if(BUILD_TESTING)
|
||||
PSX_OVERLAY_DLL_BUILD=1)
|
||||
add_test(NAME psx_icache_fastpath_test COMMAND psx_icache_fastpath_test)
|
||||
|
||||
add_executable(sio_dualshock_rumble_test
|
||||
tests/test_sio_dualshock_rumble.c
|
||||
src/sio.c)
|
||||
target_include_directories(sio_dualshock_rumble_test PRIVATE include)
|
||||
add_test(NAME sio_dualshock_rumble_test COMMAND sio_dualshock_rumble_test)
|
||||
|
||||
add_executable(gte_register_access_test
|
||||
tests/test_gte_register_access.cpp
|
||||
src/gte.cpp
|
||||
|
||||
@@ -94,6 +94,12 @@ void sio_set_pad_connected(int slot, int connected);
|
||||
* unchanged. */
|
||||
void sio_set_pad_config_capable(int slot, int capable);
|
||||
|
||||
/* Read the raw DualShock motor state selected by the guest's 0x4D rumble map
|
||||
* and most recent 0x42 poll. `small` is the fixed-strength high-frequency
|
||||
* motor byte; `large` is the variable-strength low-frequency motor byte.
|
||||
* The frontend translates these values to its host controller API. */
|
||||
void sio_get_pad_rumble(int slot, uint8_t *small, uint8_t *large);
|
||||
|
||||
/* Return current pad button state (for debug server). _slot targets either. */
|
||||
uint16_t sio_get_pad_buttons(void);
|
||||
uint16_t sio_get_pad_buttons_slot(int slot);
|
||||
|
||||
@@ -314,6 +314,10 @@ struct PlayerInput {
|
||||
bool hybrid_analog = false;
|
||||
SDL_GameController* handle = nullptr;
|
||||
SDL_JoystickID instance = -1;
|
||||
uint8_t rumble_small = 0;
|
||||
uint8_t rumble_large = 0;
|
||||
bool rumble_known = false;
|
||||
bool rumble_warned = false;
|
||||
};
|
||||
static PlayerInput g_players[2];
|
||||
/* ARGB8888 staging buffer. Sized for the active internal resolution:
|
||||
@@ -2627,10 +2631,16 @@ static void load_input_config(const char* argv0) {
|
||||
|
||||
static void close_player(PlayerInput& p) {
|
||||
if (p.handle) {
|
||||
if (p.rumble_small || p.rumble_large)
|
||||
(void)SDL_GameControllerRumble(p.handle, 0, 0, 0);
|
||||
SDL_GameControllerClose(p.handle);
|
||||
p.handle = nullptr;
|
||||
p.instance = -1;
|
||||
}
|
||||
p.rumble_small = 0;
|
||||
p.rumble_large = 0;
|
||||
p.rumble_known = false;
|
||||
p.rumble_warned = false;
|
||||
}
|
||||
|
||||
static void close_controller(void) {
|
||||
@@ -2668,6 +2678,61 @@ static void open_player(PlayerInput& p, const PlayerInput& other) {
|
||||
const char* name = SDL_GameControllerName(p.handle);
|
||||
std::fprintf(stdout, "psxrecomp runtime: opened controller for slot: %s\n",
|
||||
name ? name : "(unnamed)");
|
||||
p.rumble_known = false;
|
||||
p.rumble_warned = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Forward the emulated DualShock's two motors to the SDL controller assigned
|
||||
* to the same PSX port. The large motor is variable-strength/low-frequency;
|
||||
* the small motor is fixed-strength/high-frequency. Active effects are renewed
|
||||
* every VBlank with a short lifetime so a crash or unplug cannot leave a host
|
||||
* controller vibrating indefinitely. */
|
||||
static void update_controller_rumble(void) {
|
||||
static const bool trace = [] {
|
||||
const char* e = std::getenv("PSX_RUMBLE_TRACE");
|
||||
return e && e[0] && e[0] != '0';
|
||||
}();
|
||||
for (int s = 0; s < 2; s++) {
|
||||
PlayerInput& p = g_players[s];
|
||||
uint8_t small = 0, large = 0;
|
||||
sio_get_pad_rumble(s, &small, &large);
|
||||
if (!p.handle) {
|
||||
p.rumble_small = small;
|
||||
p.rumble_large = large;
|
||||
p.rumble_known = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool changed = !p.rumble_known || p.rumble_small != small ||
|
||||
p.rumble_large != large;
|
||||
if (small || large || (changed && p.rumble_known)) {
|
||||
const Uint16 low_frequency = (Uint16)((unsigned)large * 257u);
|
||||
const Uint16 high_frequency = small ? 0xFFFFu : 0u;
|
||||
#if defined(PSX_SDL3)
|
||||
const int rc = SDL_GameControllerRumble(
|
||||
p.handle, low_frequency, high_frequency,
|
||||
(small || large) ? 100u : 0u) ? 0 : -1;
|
||||
#else
|
||||
const int rc = SDL_GameControllerRumble(
|
||||
p.handle, low_frequency, high_frequency,
|
||||
(small || large) ? 100u : 0u);
|
||||
#endif
|
||||
if (rc != 0 && !p.rumble_warned) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp runtime: controller for slot %d rejected rumble: %s\n",
|
||||
s + 1, SDL_GetError());
|
||||
p.rumble_warned = true;
|
||||
}
|
||||
}
|
||||
if (trace && changed) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp rumble: slot=%d small=%u large=%u\n",
|
||||
s + 1, (unsigned)small, (unsigned)large);
|
||||
}
|
||||
p.rumble_small = small;
|
||||
p.rumble_large = large;
|
||||
p.rumble_known = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3765,6 +3830,7 @@ static void sdl_vblank_present(void) {
|
||||
} else {
|
||||
sample_pad_into_sio(override);
|
||||
}
|
||||
if (!g_headless) update_controller_rumble();
|
||||
|
||||
/* Latency ring: open this present cycle's slot, stamping when input was
|
||||
* sampled into SIO. Always-on; queried via the debug server "latency". */
|
||||
@@ -6813,6 +6879,10 @@ session_reboot:
|
||||
* as a game controller rather than a raw HID device. */
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
|
||||
/* SDL3 aliases the SDL2-era PS5 rumble hint to enhanced reports. Enabling
|
||||
* it also preserves DualSense rumble on the explicit SDL2 fallback. */
|
||||
SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1",
|
||||
SDL_HINT_OVERRIDE);
|
||||
/* ...but HIDAPI's Xbox sub-driver is OFF by default on Windows (Xbox pads are
|
||||
* normally RAWINPUT/XInput there). With RAWINPUT disabled above, a PHYSICAL
|
||||
* Xbox One/Series controller would be claimed by nobody -> not a GameController
|
||||
|
||||
+83
-3
@@ -51,6 +51,17 @@ static uint8_t pad_analog[2] = { 0, 0 };
|
||||
static uint8_t pad_stick[2][4] = { { 0x80, 0x80, 0x80, 0x80 },
|
||||
{ 0x80, 0x80, 0x80, 0x80 } }; /* lx,ly,rx,ry */
|
||||
|
||||
/* DualShock command 0x4D maps the six writable bytes in a 0x42 poll onto the
|
||||
* two motors: 0x00 = small/high-frequency, 0x01 = large/low-frequency,
|
||||
* 0xFF = unused. The map powers up unassigned and is echoed back while a new
|
||||
* map is latched, matching the physical pad/Mednafen protocol. */
|
||||
static uint8_t pad_rumble_map[2][6] = {
|
||||
{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF },
|
||||
{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF },
|
||||
};
|
||||
static uint8_t pad_rumble_small[2] = { 0, 0 };
|
||||
static uint8_t pad_rumble_large[2] = { 0, 0 };
|
||||
|
||||
/* Analog-mode lock, per slot. A real DualShock's config command 0x44 0x..02/0x03
|
||||
* locks/unlocks the mode (dualshock.cpp:714-725); a locked pad ignores the
|
||||
* physical analog button (dualshock.cpp:203). We emulate the analog button via
|
||||
@@ -543,6 +554,9 @@ void sio_init(void) {
|
||||
pad_response_idx = 0;
|
||||
pad_current_cmd = 0;
|
||||
pad_buttons[0] = pad_buttons[1] = 0xFFFF;
|
||||
memset(pad_rumble_map, 0xFF, sizeof(pad_rumble_map));
|
||||
pad_rumble_small[0] = pad_rumble_small[1] = 0;
|
||||
pad_rumble_large[0] = pad_rumble_large[1] = 0;
|
||||
pad_in_config[0] = pad_in_config[1] = 0; /* clear stale config latch on reset */
|
||||
pad_type_req[0] = pad_type_req[1] = -1; /* no pending host type change */
|
||||
analog_mode_locked[0] = analog_mode_locked[1] = 0; /* unlocked on reset */
|
||||
@@ -610,7 +624,11 @@ void sio_connect_pad(int slot) {
|
||||
void sio_set_pad_connected(int slot, int connected) {
|
||||
if (slot < 0 || slot > 1) return;
|
||||
if (connected) pad_connected |= (uint8_t)(1 << slot);
|
||||
else pad_connected &= (uint8_t)~(1 << slot);
|
||||
else {
|
||||
pad_connected &= (uint8_t)~(1 << slot);
|
||||
pad_rumble_small[slot] = 0;
|
||||
pad_rumble_large[slot] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void sio_set_pad_config_capable(int slot, int capable) {
|
||||
@@ -618,7 +636,21 @@ void sio_set_pad_config_capable(int slot, int capable) {
|
||||
pad_supports_config[slot] = capable ? 1 : 0;
|
||||
/* A plain digital pad can never be in config mode; clear any stale latch so
|
||||
* the next poll reports the digital id (0x41), not the config id (0xF3). */
|
||||
if (!capable) pad_in_config[slot] = 0;
|
||||
if (!capable) {
|
||||
pad_in_config[slot] = 0;
|
||||
pad_rumble_small[slot] = 0;
|
||||
pad_rumble_large[slot] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void sio_get_pad_rumble(int slot, uint8_t *small, uint8_t *large) {
|
||||
uint8_t s = 0, l = 0;
|
||||
if (slot >= 0 && slot <= 1 && (pad_connected & (1 << slot))) {
|
||||
s = pad_rumble_small[slot];
|
||||
l = pad_rumble_large[slot];
|
||||
}
|
||||
if (small) *small = s;
|
||||
if (large) *large = l;
|
||||
}
|
||||
|
||||
void sio_set_pad_state(uint16_t buttons) {
|
||||
@@ -813,6 +845,10 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
* (axis5_sio_controller.md D8). */
|
||||
if (tx_byte == 0x45)
|
||||
pad_response[3] = pad_analog[selected_slot] ? 0x01 : 0x00;
|
||||
/* 0x4D returns the previous six-byte motor map while latching the
|
||||
* replacement bytes later in this same transaction. */
|
||||
if (tx_byte == 0x4D)
|
||||
memcpy(&pad_response[2], pad_rumble_map[selected_slot], 6);
|
||||
pad_response_len = 8;
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
@@ -830,6 +866,25 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
break;
|
||||
|
||||
case PAD_SEND_RESPONSE:
|
||||
/* The six data bytes after 0x42's leading 0x00 occupy response indexes
|
||||
* 2..7. Route each through the map negotiated by command 0x4D. */
|
||||
if (pad_current_cmd == 0x42 &&
|
||||
pad_response_idx >= 2 && pad_response_idx < 8) {
|
||||
const unsigned map_index = (unsigned)pad_response_idx - 2u;
|
||||
const uint8_t motor = pad_rumble_map[selected_slot][map_index];
|
||||
if (motor == 0x00)
|
||||
pad_rumble_small[selected_slot] = tx_byte;
|
||||
else if (motor == 0x01)
|
||||
pad_rumble_large[selected_slot] = tx_byte;
|
||||
}
|
||||
/* 0x4D uses the same six wire positions. pad_response[] was populated
|
||||
* with the old values before the transaction, so updating the live map
|
||||
* here preserves the required echo-before-write behavior. */
|
||||
if (pad_current_cmd == 0x4D &&
|
||||
pad_response_idx >= 2 && pad_response_idx < 8) {
|
||||
const unsigned map_index = (unsigned)pad_response_idx - 2u;
|
||||
pad_rumble_map[selected_slot][map_index] = tx_byte;
|
||||
}
|
||||
/* For 0x43 (enter/exit config), the data byte selecting enter(0x01)/
|
||||
* exit(0x00) arrives paired with response index 2. Latch the new config
|
||||
* state; it takes effect from the next transaction (the ID byte already
|
||||
@@ -2166,6 +2221,10 @@ static int sio_snap_emit(PstW *w) {
|
||||
!pst_w_u8(w, sio_irq_pending_delay) || !pst_w_u8(w, sio_irq_pending_mc_state) ||
|
||||
!pst_w_u32(w, sio_irq_pending_byte_seq))
|
||||
return 0;
|
||||
if (!pst_w_bytes(w, pad_rumble_map, sizeof(pad_rumble_map)) ||
|
||||
!pst_w_bytes(w, pad_rumble_small, sizeof(pad_rumble_small)) ||
|
||||
!pst_w_bytes(w, pad_rumble_large, sizeof(pad_rumble_large)))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2233,6 +2292,22 @@ static int sio_snap_parse(PstR *r) {
|
||||
!pst_r_u8(r, &sio_irq_pending_delay) || !pst_r_u8(r, &sio_irq_pending_mc_state) ||
|
||||
!pst_r_u32(r, &sio_irq_pending_byte_seq))
|
||||
return 0;
|
||||
/* v1.0.4 and older snapshots end here. A mid-game legacy state cannot tell
|
||||
* us the negotiated map, so assume the standard 0x00/0x01 layout used by
|
||||
* commercial DualShock games while leaving both motors stopped. */
|
||||
if (r->p == r->end) {
|
||||
memset(pad_rumble_map, 0xFF, sizeof(pad_rumble_map));
|
||||
pad_rumble_map[0][0] = pad_rumble_map[1][0] = 0x00;
|
||||
pad_rumble_map[0][1] = pad_rumble_map[1][1] = 0x01;
|
||||
pad_rumble_small[0] = pad_rumble_small[1] = 0;
|
||||
pad_rumble_large[0] = pad_rumble_large[1] = 0;
|
||||
return 1;
|
||||
}
|
||||
if (!pst_r_bytes(r, pad_rumble_map, sizeof(pad_rumble_map)) ||
|
||||
!pst_r_bytes(r, pad_rumble_small, sizeof(pad_rumble_small)) ||
|
||||
!pst_r_bytes(r, pad_rumble_large, sizeof(pad_rumble_large)))
|
||||
return 0;
|
||||
if (r->p != r->end) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2252,7 +2327,12 @@ void sio_snapshot_write(uint8_t *p) {
|
||||
|
||||
int sio_snapshot_read(const uint8_t *p, uint32_t len) {
|
||||
PstR r;
|
||||
if (len != sio_snapshot_bytes()) return 0;
|
||||
const uint32_t current = sio_snapshot_bytes();
|
||||
const uint32_t rumble_bytes = (uint32_t)(sizeof(pad_rumble_map) +
|
||||
sizeof(pad_rumble_small) +
|
||||
sizeof(pad_rumble_large));
|
||||
if (len != current && (len > current || len + rumble_bytes != current))
|
||||
return 0;
|
||||
pst_r_init(&r, p, len);
|
||||
return sio_snap_parse(&r);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/* DualShock rumble protocol regression: 0x4D map negotiation must feed the
|
||||
* small/large motor bytes from later 0x42 polls to the frontend-facing state. */
|
||||
|
||||
#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 g_debug_current_func_addr = 0;
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; }
|
||||
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 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));
|
||||
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);
|
||||
sio_write(SIO_CTRL, ctrl | CTRL_ACK);
|
||||
return rx;
|
||||
}
|
||||
|
||||
static void finish_six_data_bytes(int slot, unsigned already_sent) {
|
||||
while (already_sent++ < 6) (void)xchg(slot, 0x00);
|
||||
}
|
||||
|
||||
static void enter_config(int slot) {
|
||||
EXPECT("enter.prefix", 0xFF, xchg(slot, 0x01));
|
||||
EXPECT("enter.id", 0x73, xchg(slot, 0x43));
|
||||
EXPECT("enter.ack", 0x5A, xchg(slot, 0x00));
|
||||
(void)xchg(slot, 0x01);
|
||||
finish_six_data_bytes(slot, 1);
|
||||
}
|
||||
|
||||
static void set_rumble_map(int slot, const uint8_t map[6],
|
||||
const uint8_t expected_old[6]) {
|
||||
EXPECT("map.prefix", 0xFF, xchg(slot, 0x01));
|
||||
EXPECT("map.id", 0xF3, xchg(slot, 0x4D));
|
||||
EXPECT("map.ack", 0x5A, xchg(slot, 0x00));
|
||||
for (int i = 0; i < 6; i++)
|
||||
EXPECT("map.echo", expected_old[i], xchg(slot, map[i]));
|
||||
}
|
||||
|
||||
static void exit_config(int slot) {
|
||||
EXPECT("exit.prefix", 0xFF, xchg(slot, 0x01));
|
||||
EXPECT("exit.id", 0xF3, xchg(slot, 0x43));
|
||||
EXPECT("exit.ack", 0x5A, xchg(slot, 0x00));
|
||||
(void)xchg(slot, 0x00);
|
||||
finish_six_data_bytes(slot, 1);
|
||||
}
|
||||
|
||||
static void poll_with_motors(int slot, uint8_t small, uint8_t large) {
|
||||
EXPECT("poll.prefix", 0xFF, xchg(slot, 0x01));
|
||||
EXPECT("poll.id", 0x73, xchg(slot, 0x42));
|
||||
EXPECT("poll.ack", 0x5A, xchg(slot, 0x00));
|
||||
(void)xchg(slot, small);
|
||||
(void)xchg(slot, large);
|
||||
finish_six_data_bytes(slot, 2);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static const uint8_t unassigned[6] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
||||
};
|
||||
static const uint8_t standard[6] = {
|
||||
0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF
|
||||
};
|
||||
uint8_t small = 0, large = 0;
|
||||
|
||||
sio_init();
|
||||
sio_connect_pad(0);
|
||||
sio_set_pad_analog(0, 1, 0x80, 0x80, 0x80, 0x80);
|
||||
sio_set_pad_config_capable(0, 1);
|
||||
|
||||
enter_config(0);
|
||||
set_rumble_map(0, standard, unassigned);
|
||||
set_rumble_map(0, standard, standard);
|
||||
exit_config(0);
|
||||
|
||||
poll_with_motors(0, 0x01, 0x80);
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("rumble.small.on", 0x01, small);
|
||||
EXPECT("rumble.large.strength", 0x80, large);
|
||||
|
||||
const uint32_t snapshot_len = sio_snapshot_bytes();
|
||||
uint8_t *snapshot = (uint8_t *)malloc(snapshot_len);
|
||||
if (!snapshot) {
|
||||
fprintf(stderr, "FAIL snapshot allocation\n");
|
||||
return 1;
|
||||
}
|
||||
sio_snapshot_write(snapshot);
|
||||
|
||||
poll_with_motors(0, 0x00, 0x00);
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("rumble.small.off", 0x00, small);
|
||||
EXPECT("rumble.large.off", 0x00, large);
|
||||
|
||||
EXPECT("snapshot.current.read", 1,
|
||||
sio_snapshot_read(snapshot, snapshot_len));
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("snapshot.current.small", 0x01, small);
|
||||
EXPECT("snapshot.current.large", 0x80, large);
|
||||
|
||||
/* The final 16 bytes are the new rumble fields. Truncating them models a
|
||||
* v1.0.4 SIO section and must load safely with both motors stopped. */
|
||||
EXPECT("snapshot.legacy.read", 1,
|
||||
sio_snapshot_read(snapshot, snapshot_len - 16));
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("snapshot.legacy.small", 0x00, small);
|
||||
EXPECT("snapshot.legacy.large", 0x00, large);
|
||||
free(snapshot);
|
||||
|
||||
poll_with_motors(0, 0x01, 0xFF);
|
||||
sio_set_pad_connected(0, 0);
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("rumble.disconnect.small", 0x00, small);
|
||||
EXPECT("rumble.disconnect.large", 0x00, large);
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "DualShock rumble protocol: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "DualShock rumble protocol: passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user