mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-21 00:00:26 -05:00
Up to four controllers, one per signed-in PS5 user
The console pairs each controller with a signed-in user, and scePadOpen
opens a user's controller, so RetroArch read only the first player's.
The joypad driver now connects controllers the way ProsperoEden does
(PS5_ProsperoEden b919847, headless/pad.cpp):
- Port 0 (player 1) is the user who started the title, and stays theirs.
- About once a second sceUserServiceGetLoginUserIdList is read again. A
new user's controller takes the first free port (1 to 3); a port is
let go, and announced gone, when its user signs out. A pad the process
already holds is reused through scePadGetHandle.
- Each port is announced to RetroArch's autoconfiguration on its own and
has its own buttons, axes, rumble and haptic stream. The pad script
still plays port 0.
The console's config already maps players 1-4 to joypad indexes 0-3.
For test runs, tools/run-title.sh --pad-monitor arms
/app0/pad-monitor.txt, which traces each port's whole button word as it
changes, including the bit that says the system holds the controller.
Evidence: evidence/multi-controller/ (capture.json, expectation.json;
console user ids masked). On the base PS5 13.40 with two DualSense
controllers and two signed-in users (build 95a82988):
run 211620: ports 0 and 1 opened, each with its haptic stream, and
both reported connected
the owner's own launch from the home screen: the launching user's
controller became port 0 and the other port 1; both were opened again
after RetroArch re-initialised input, and a racing game played with
both controllers
The committed source differs from build 95a82988 only in the monitor's
trace format, which the host test covers; that build was not run on the
console.
Host test: tests/test_input_ps5.py PASS (the four-player scenario: join,
separate input and rumble, sign-out, rejoin, the first player kept, an
open failure, a full house, the monitor, re-announcement after a reset).
Command:
tools/run-title.sh --no-build --no-deploy --pad-monitor --watch 120
Gates: tools/verify.sh PASS (format unit build integration evidence).
This commit is contained in:
1 parent
c56a9aa77f
commit
4788d35076
6 files changed
+448
-100
No files matched your search
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"step": "multi-controller",
|
||||
"recorded": "2026-10-05T01:23:34Z",
|
||||
"console_run": "base PS5 13.40, run 211620 and the owner's own session",
|
||||
"revision": "5ab84a1 + four controller ports",
|
||||
"command": "tools/run-title.sh --no-build --no-deploy --pad-monitor --watch 120; the owner's launch from the home screen",
|
||||
"raw_capture": "klog/multipad/summary.txt",
|
||||
"raw_lines": 15,
|
||||
"raw_bytes": 1248,
|
||||
"lines": [
|
||||
"==> base PS5 13.40, two DualSense controllers, two signed-in users (user ids masked: A launched first, B the second)",
|
||||
"monitored run 211620 (tools/run-title.sh --no-build --no-deploy --pad-monitor --watch 120, build 95a82988):",
|
||||
" input: pad opened, port=0 user=A haptic_thread=1 format=s16",
|
||||
" input: signed-in users A B -1 -1",
|
||||
" input: pad opened, port=1 user=B haptic_thread=1 format=s16",
|
||||
" input: port 0: user A's controller connected",
|
||||
" input: port 1: user B's controller connected",
|
||||
" monitor: no button changes (no buttons were pressed in that run)",
|
||||
"owner's session (launched from the home screen with B's controller, picker -> RetroArch, build 95a82988):",
|
||||
" input: pad opened, port=0 user=B; signed-in users B A -1 -1; pad opened, port=1 user=A",
|
||||
" input: port 0: user B's controller connected; port 1: user A's controller connected",
|
||||
" RetroArch re-initialised its input driver: both ports opened and connected again",
|
||||
" owner: a racing game played with two controllers works",
|
||||
"host test: PS5 joypad: raw binding capture, axes, user mappings, rumble, poll retention, lifecycle, four players by signed-in user and the script's STOP and OPTION PASS",
|
||||
"committed source (build cafaf27f): the monitor's trace format only differs from 95a82988; not run on the console"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"step": "multi-controller",
|
||||
"note": "Up to four controllers, one per signed-in user, as ProsperoEden connects them: port 0 the user who started the title, later users the first free port, a port let go at sign-out. Two controllers on the base PS5 opened as ports 0 and 1 with their own haptic streams, and the owner played a racing game with both.",
|
||||
"must_contain": [
|
||||
"input: signed-in users A B -1 -1",
|
||||
"input: pad opened, port=1 user=B haptic_thread=1 format=s16",
|
||||
"input: port 1: user B's controller connected",
|
||||
"four players by signed-in user"
|
||||
],
|
||||
"must_not_contain": [],
|
||||
"human_check": "The owner played a racing game with two controllers on the base PS5 (build 95a82988) and reported that both work."
|
||||
}
|
||||
+248
-78
@@ -16,6 +16,16 @@
|
||||
* same offsets, including `connected` at 0x4c and the timestamp at 0x50. The static
|
||||
* assertions are what keeps a wrong layout from compiling quietly.
|
||||
*
|
||||
* Up to four controllers, as ../PS5_ProsperoEden connects them: the console pairs
|
||||
* each controller with a signed-in user, and scePadOpen opens a user's controller.
|
||||
* Port 0 (player 1) is the user who started the title, and stays theirs. About once
|
||||
* a second the signed-in users are read again (sceUserServiceGetLoginUserIdList): a
|
||||
* new user's controller takes the first free port, 1 to 3, and a port is let go when
|
||||
* its user signs out. Each port is announced to RetroArch, rumbles and streams its
|
||||
* own haptics, as the first one always has. A test run can trace each port's buttons
|
||||
* as they change (/app0/pad-monitor.txt), which is how two controllers are told apart
|
||||
* in a log.
|
||||
*
|
||||
* A run without a person at the pad can script it: /app0/pad-script.txt, when
|
||||
* present, holds one press a line, `<seconds> <BUTTON>[+<BUTTON>...] [<held
|
||||
* seconds>]`, timed from the first poll, with RetroPad names (B Y SELECT START
|
||||
@@ -114,7 +124,16 @@ extern "C"
|
||||
std::int32_t scePadSetVibrationMode(std::int32_t handle, std::int32_t mode);
|
||||
std::int32_t sceUserServiceInitialize(const void *params);
|
||||
std::int32_t sceUserServiceGetInitialUser(std::int32_t *user_id);
|
||||
/* The signed-in users, at most four; unused entries are -1 (the shape
|
||||
* ../PS5_ProsperoEden reads them with on the console). */
|
||||
struct SceUserServiceLoginUserIdList
|
||||
{
|
||||
std::int32_t user_id[4];
|
||||
};
|
||||
std::int32_t sceUserServiceGetLoginUserIdList(SceUserServiceLoginUserIdList *list);
|
||||
std::int32_t sceUserServiceTerminate();
|
||||
/* The handle this process already holds for a user's controller. */
|
||||
std::int32_t scePadGetHandle(std::int32_t user_id, std::int32_t port_type, std::int32_t index);
|
||||
std::int32_t sceKernelUsleep(std::uint32_t microseconds);
|
||||
/* The audio service. The same declarations and call shape as
|
||||
* src/audio_ps5.cpp, which follows ProsperoLight's moonlight_stream.cpp.
|
||||
@@ -177,6 +196,12 @@ static_assert(offsetof(PadSample, timestamp_us) == 0x50, "the timestamp sits at
|
||||
constexpr int sample_capacity = 64;
|
||||
constexpr std::int32_t pad_open_attempts = 10;
|
||||
constexpr std::uint32_t pad_open_retry_microseconds = 100000;
|
||||
/* A console signs in at most four users, each with one controller. */
|
||||
constexpr unsigned max_pads = 4;
|
||||
/* How often the signed-in users are read again. */
|
||||
constexpr auto user_scan_interval = std::chrono::seconds(1);
|
||||
/* The pad monitor's budget of trace lines for a run. */
|
||||
constexpr unsigned monitor_line_budget = 600;
|
||||
/* The stick bytes run 0..255 with 128 centred. */
|
||||
constexpr int stick_centre = 128;
|
||||
|
||||
@@ -192,6 +217,8 @@ namespace
|
||||
{
|
||||
struct PadState
|
||||
{
|
||||
std::int32_t user_id = -1;
|
||||
unsigned port = 0;
|
||||
std::int32_t handle = -1;
|
||||
PadSample samples[sample_capacity];
|
||||
/* How many entries of `samples` the last read actually filled. A read returns
|
||||
@@ -214,11 +241,28 @@ struct PadState
|
||||
pthread_t haptic_thread{};
|
||||
bool haptic_thread_started = false;
|
||||
std::atomic<bool> haptic_stop{false};
|
||||
bool owns_user_service = false;
|
||||
bool announced = false;
|
||||
/* The button word the pad monitor last traced for this port. */
|
||||
std::uint32_t monitored_buttons = 0;
|
||||
};
|
||||
|
||||
PadState *active_pad = nullptr;
|
||||
/* The controller on each port, or null. */
|
||||
PadState *pads[max_pads] = {};
|
||||
/* The joypad driver's handle for RetroArch: the driver stays registered with no
|
||||
* controller at all, and reports nothing until one comes. */
|
||||
int joypad_cookie;
|
||||
bool joypad_ready = false;
|
||||
bool owns_user_service = false;
|
||||
/* scePadInit answered, so controllers can be opened. */
|
||||
bool pad_service_ready = false;
|
||||
std::chrono::steady_clock::time_point next_user_scan{};
|
||||
/* The signed-in users the last scan read, traced when they change. */
|
||||
std::int32_t known_users[4] = {-1, -1, -1, -1};
|
||||
/* A user whose controller would not open, traced once rather than every scan. */
|
||||
std::int32_t unopened_user = -1;
|
||||
/* /app0/pad-monitor.txt: trace each port's buttons as they change. */
|
||||
bool pad_monitor = false;
|
||||
unsigned monitor_lines = 0;
|
||||
|
||||
/* The pad script (see the top of this file). */
|
||||
struct ScriptPress
|
||||
@@ -604,46 +648,51 @@ void *haptic_worker(void *opaque) noexcept
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void *open_pad() noexcept
|
||||
/* Opens a user's controller on a port: the pad, its vibration port and haptic
|
||||
* stream. Null when the pad would not open. */
|
||||
PadState *open_pad(std::int32_t user_id, unsigned port, std::int32_t attempts) noexcept
|
||||
{
|
||||
auto *state = new (std::nothrow) PadState();
|
||||
if (state == nullptr)
|
||||
{
|
||||
ps5_input_trace("input: the driver state could not be allocated");
|
||||
ps5_input_trace("input: the pad state could not be allocated");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
state->owns_user_service = sceUserServiceInitialize(nullptr) == 0;
|
||||
|
||||
std::int32_t user_id = -1;
|
||||
if (sceUserServiceGetInitialUser(&user_id) < 0)
|
||||
{
|
||||
ps5_input_trace("input: sceUserServiceGetInitialUser found no user; no pad will be read");
|
||||
return state; /* the driver stays alive and reports nothing */
|
||||
}
|
||||
if (scePadInit() < 0)
|
||||
{
|
||||
ps5_input_trace("input: scePadInit failed; no pad will be read");
|
||||
return state;
|
||||
}
|
||||
state->user_id = user_id;
|
||||
state->port = port;
|
||||
/* The pad is not always there the first time it is asked for - a title started
|
||||
* from the shell can arrive before the pad service has published the device -
|
||||
* so the open is retried, as ../ProsperoLight retries it. */
|
||||
for (std::int32_t attempt = 0; attempt < pad_open_attempts; ++attempt)
|
||||
* so the first controller's open is retried, as ../ProsperoLight retries it. */
|
||||
for (std::int32_t attempt = 0; attempt < attempts; ++attempt)
|
||||
{
|
||||
state->handle = scePadOpen(user_id, 0, 0, nullptr);
|
||||
if (state->handle >= 0)
|
||||
if (state->handle >= 0 || attempt + 1 == attempts)
|
||||
break;
|
||||
(void)sceKernelUsleep(pad_open_retry_microseconds);
|
||||
}
|
||||
/* A handle this process already holds for the user is reused. */
|
||||
if (state->handle < 0)
|
||||
{
|
||||
char line[176];
|
||||
std::snprintf(line, sizeof(line),
|
||||
"input: scePadOpen failed after %d attempts, handle=%d; no input this run",
|
||||
static_cast<int>(pad_open_attempts), state->handle);
|
||||
ps5_input_trace(line);
|
||||
return state;
|
||||
const std::int32_t held = scePadGetHandle(user_id, 0, 0);
|
||||
if (held >= 0)
|
||||
state->handle = held;
|
||||
}
|
||||
if (state->handle < 0)
|
||||
{
|
||||
if (user_id != unopened_user)
|
||||
{
|
||||
char line[176];
|
||||
std::snprintf(line, sizeof(line),
|
||||
"input: port %u: scePadOpen for user %d failed after %d attempts, "
|
||||
"handle=%d",
|
||||
port, static_cast<int>(user_id), static_cast<int>(attempts),
|
||||
state->handle);
|
||||
ps5_input_trace(line);
|
||||
unopened_user = user_id;
|
||||
}
|
||||
pthread_mutex_destroy(&state->rumble_mutex);
|
||||
delete state;
|
||||
return nullptr;
|
||||
}
|
||||
/* Try the vibration audio port. When it opens, the pad goes to advanced
|
||||
* mode (1), where the actuators follow the PCM stream the worker feeds -
|
||||
@@ -685,9 +734,9 @@ void *open_pad() noexcept
|
||||
{
|
||||
char line[256];
|
||||
std::snprintf(line, sizeof(line),
|
||||
"input: pad opened, user=%d handle=%d haptic_port=%d haptic_thread=%d "
|
||||
"vibration_mode=%d format=%s thread_result=%d",
|
||||
static_cast<int>(user_id), state->handle, state->haptic_port,
|
||||
"input: pad opened, port=%u user=%d handle=%d haptic_port=%d "
|
||||
"haptic_thread=%d vibration_mode=%d format=%s thread_result=%d",
|
||||
port, static_cast<int>(user_id), state->handle, state->haptic_port,
|
||||
state->haptic_thread_started ? 1 : 0, mode_result,
|
||||
state->haptic_float ? "f32" : "s16", thread_result);
|
||||
ps5_input_trace(line);
|
||||
@@ -741,32 +790,115 @@ void close_pad(void *data) noexcept
|
||||
(void)scePadClose(state->handle);
|
||||
state->handle = -1;
|
||||
}
|
||||
if (state->owns_user_service)
|
||||
{
|
||||
(void)sceUserServiceTerminate();
|
||||
state->owns_user_service = false;
|
||||
}
|
||||
pthread_mutex_destroy(&state->rumble_mutex);
|
||||
delete state;
|
||||
}
|
||||
|
||||
/* Lets a port's controller go: RetroArch is told it left, then it is closed. */
|
||||
void release_port(unsigned port, const char *why) noexcept
|
||||
{
|
||||
PadState *state = pads[port];
|
||||
if (state == nullptr)
|
||||
return;
|
||||
if (state->announced)
|
||||
input_autoconfigure_disconnect(port, "PS5 Controller");
|
||||
char line[128];
|
||||
std::snprintf(line, sizeof(line), "input: port %u: user %d's controller let go (%s)", port,
|
||||
static_cast<int>(state->user_id), why);
|
||||
ps5_input_trace(line);
|
||||
pads[port] = nullptr;
|
||||
close_pad(state);
|
||||
}
|
||||
|
||||
/* Reads the signed-in users again: a port is let go when its user signed out
|
||||
* (port 0 stays with the user who started the title), and a new user's
|
||||
* controller takes the first free port. */
|
||||
void scan_users() noexcept
|
||||
{
|
||||
SceUserServiceLoginUserIdList list{{-1, -1, -1, -1}};
|
||||
if (!pad_service_ready || sceUserServiceGetLoginUserIdList(&list) < 0)
|
||||
return;
|
||||
if (std::memcmp(list.user_id, known_users, sizeof(known_users)) != 0)
|
||||
{
|
||||
std::memcpy(known_users, list.user_id, sizeof(known_users));
|
||||
char line[128];
|
||||
std::snprintf(line, sizeof(line), "input: signed-in users %d %d %d %d",
|
||||
static_cast<int>(list.user_id[0]), static_cast<int>(list.user_id[1]),
|
||||
static_cast<int>(list.user_id[2]), static_cast<int>(list.user_id[3]));
|
||||
ps5_input_trace(line);
|
||||
}
|
||||
const auto signed_in = [&list](std::int32_t user)
|
||||
{
|
||||
for (const std::int32_t id : list.user_id)
|
||||
if (id >= 0 && id == user)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
for (unsigned port = 1; port < max_pads; ++port)
|
||||
if (pads[port] != nullptr && !signed_in(pads[port]->user_id))
|
||||
release_port(port, "signed out");
|
||||
for (const std::int32_t user : list.user_id)
|
||||
{
|
||||
if (user < 0)
|
||||
continue;
|
||||
bool known = false;
|
||||
for (const PadState *state : pads)
|
||||
known |= state != nullptr && state->user_id == user;
|
||||
if (known)
|
||||
continue;
|
||||
for (unsigned port = 0; port < max_pads; ++port)
|
||||
if (pads[port] == nullptr)
|
||||
{
|
||||
pads[port] = open_pad(user, port, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void *joypad_init(void *) noexcept
|
||||
{
|
||||
if (!active_pad)
|
||||
if (!joypad_ready)
|
||||
{
|
||||
active_pad = state_of(open_pad());
|
||||
if (active_pad && script_count == 0)
|
||||
joypad_ready = true;
|
||||
owns_user_service = sceUserServiceInitialize(nullptr) == 0;
|
||||
std::int32_t user_id = -1;
|
||||
if (sceUserServiceGetInitialUser(&user_id) < 0)
|
||||
ps5_input_trace("input: sceUserServiceGetInitialUser found no user");
|
||||
pad_service_ready = scePadInit() >= 0;
|
||||
if (!pad_service_ready)
|
||||
ps5_input_trace("input: scePadInit failed; no pad will be read");
|
||||
else if (user_id >= 0)
|
||||
pads[0] = open_pad(user_id, 0, pad_open_attempts);
|
||||
unopened_user = -1;
|
||||
scan_users();
|
||||
next_user_scan = std::chrono::steady_clock::now() + user_scan_interval;
|
||||
if (script_count == 0)
|
||||
load_script();
|
||||
std::FILE *monitor = std::fopen("/app0/pad-monitor.txt", "rb");
|
||||
pad_monitor = monitor != nullptr;
|
||||
monitor_lines = 0;
|
||||
if (monitor != nullptr)
|
||||
std::fclose(monitor);
|
||||
}
|
||||
if (active_pad)
|
||||
ps5_input_trace("input: ps5 joypad registered (16 buttons, 6 axes)");
|
||||
return active_pad;
|
||||
ps5_input_trace("input: ps5 joypad registered (4 ports, 16 buttons, 6 axes)");
|
||||
return &joypad_cookie;
|
||||
}
|
||||
|
||||
void joypad_destroy() noexcept
|
||||
{
|
||||
close_pad(active_pad);
|
||||
active_pad = nullptr;
|
||||
for (PadState *&state : pads)
|
||||
{
|
||||
close_pad(state);
|
||||
state = nullptr;
|
||||
}
|
||||
if (owns_user_service)
|
||||
{
|
||||
(void)sceUserServiceTerminate();
|
||||
owns_user_service = false;
|
||||
}
|
||||
joypad_ready = false;
|
||||
pad_service_ready = false;
|
||||
std::memset(known_users, 0xff, sizeof(known_users));
|
||||
}
|
||||
|
||||
/* The core and content /app0/args.txt names: the line after -L, and the first
|
||||
@@ -1040,40 +1172,76 @@ void run_script_actions() noexcept
|
||||
}
|
||||
}
|
||||
|
||||
/* The controller on a port, or null. */
|
||||
PadState *pad_at(unsigned port) noexcept
|
||||
{
|
||||
return port < max_pads ? pads[port] : nullptr;
|
||||
}
|
||||
|
||||
void joypad_poll() noexcept
|
||||
{
|
||||
run_script_actions();
|
||||
poll_pad(active_pad);
|
||||
if (!active_pad)
|
||||
return;
|
||||
// Do not announce a disconnect while the shell temporarily intercepts input.
|
||||
bool connected = false;
|
||||
const PadSample *latest = nullptr;
|
||||
for (int i = 0; i < active_pad->sample_count; ++i)
|
||||
if (!latest || active_pad->samples[i].timestamp_us > latest->timestamp_us)
|
||||
latest = &active_pad->samples[i];
|
||||
connected = (latest && latest->connected) || script_count != 0;
|
||||
if (connected != active_pad->announced)
|
||||
if (pad_service_ready && std::chrono::steady_clock::now() >= next_user_scan)
|
||||
{
|
||||
active_pad->announced = connected;
|
||||
if (connected)
|
||||
input_autoconfigure_connect("PS5 Controller", nullptr, nullptr, "ps5", 0, 0, 0);
|
||||
else
|
||||
input_autoconfigure_disconnect(0, "PS5 Controller");
|
||||
scan_users();
|
||||
next_user_scan = std::chrono::steady_clock::now() + user_scan_interval;
|
||||
}
|
||||
for (unsigned port = 0; port < max_pads; ++port)
|
||||
{
|
||||
PadState *state = pads[port];
|
||||
if (state == nullptr)
|
||||
continue;
|
||||
poll_pad(state);
|
||||
// Do not announce a disconnect while the shell temporarily intercepts input.
|
||||
const PadSample *latest = nullptr;
|
||||
for (int i = 0; i < state->sample_count; ++i)
|
||||
if (!latest || state->samples[i].timestamp_us > latest->timestamp_us)
|
||||
latest = &state->samples[i];
|
||||
const bool connected = (latest && latest->connected) || (port == 0 && script_count != 0);
|
||||
if (connected != state->announced)
|
||||
{
|
||||
state->announced = connected;
|
||||
if (connected)
|
||||
input_autoconfigure_connect("PS5 Controller", nullptr, nullptr, "ps5", port, 0, 0);
|
||||
else
|
||||
input_autoconfigure_disconnect(port, "PS5 Controller");
|
||||
char line[96];
|
||||
std::snprintf(line, sizeof(line), "input: port %u: user %d's controller %s", port,
|
||||
static_cast<int>(state->user_id),
|
||||
connected ? "connected" : "disconnected");
|
||||
ps5_input_trace(line);
|
||||
}
|
||||
// The monitor traces the newest sample's whole button word, the bit that
|
||||
// says the system has the controller (its menus, a sign-in) included.
|
||||
if (pad_monitor && latest != nullptr && latest->buttons != state->monitored_buttons &&
|
||||
monitor_lines < monitor_line_budget)
|
||||
{
|
||||
state->monitored_buttons = latest->buttons;
|
||||
++monitor_lines;
|
||||
char line[128];
|
||||
std::snprintf(
|
||||
line, sizeof(line), "input: monitor port %u user %d buttons 0x%08x%s", port,
|
||||
static_cast<int>(state->user_id), static_cast<unsigned>(latest->buttons),
|
||||
(latest->buttons & pad_button_intercepted) != 0 ? " (the system has it)" : "");
|
||||
ps5_input_trace(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool joypad_query(unsigned port) noexcept
|
||||
{
|
||||
return port == 0 && active_pad && active_pad->announced;
|
||||
const PadState *state = pad_at(port);
|
||||
return state != nullptr && state->announced;
|
||||
}
|
||||
|
||||
std::uint32_t joypad_buttons(unsigned port) noexcept
|
||||
{
|
||||
if (port != 0 || !active_pad)
|
||||
PadState *state = pad_at(port);
|
||||
if (state == nullptr)
|
||||
return 0;
|
||||
const std::uint32_t scripted = script_buttons() & 0xffff;
|
||||
const PadSample *sample = newest_sample(*active_pad);
|
||||
// The pad script plays the first controller.
|
||||
const std::uint32_t scripted = port == 0 ? script_buttons() & 0xffff : 0;
|
||||
const PadSample *sample = newest_sample(*state);
|
||||
if (!sample)
|
||||
return scripted;
|
||||
auto mask = pad_buttons_to_retropad(sample->buttons) | scripted;
|
||||
@@ -1097,12 +1265,13 @@ void joypad_get_buttons(unsigned port, input_bits_t *bits) noexcept
|
||||
|
||||
std::int16_t joypad_axis(unsigned port, std::uint32_t axis) noexcept
|
||||
{
|
||||
if (port != 0 || !active_pad || axis == AXIS_NONE)
|
||||
PadState *state = pad_at(port);
|
||||
if (state == nullptr || axis == AXIS_NONE)
|
||||
return 0;
|
||||
bool negative = AXIS_NEG_GET(axis) < 6;
|
||||
unsigned index = negative ? AXIS_NEG_GET(axis) : AXIS_POS_GET(axis);
|
||||
int value = script_axis(index);
|
||||
const PadSample *sample = newest_sample(*active_pad);
|
||||
int value = port == 0 ? script_axis(index) : 0;
|
||||
const PadSample *sample = newest_sample(*state);
|
||||
if (value != 0)
|
||||
;
|
||||
else if (!sample)
|
||||
@@ -1158,7 +1327,7 @@ std::int16_t joypad_state(rarch_joypad_info_t *info, const retro_keybind *binds,
|
||||
|
||||
const char *joypad_name(unsigned port) noexcept
|
||||
{
|
||||
return port == 0 ? "PS5 Controller" : nullptr;
|
||||
return port == 0 || pad_at(port) != nullptr ? "PS5 Controller" : nullptr;
|
||||
}
|
||||
|
||||
/* RetroArch hands rumble in 0..65535 per effect; the pad wants 0..255 per
|
||||
@@ -1167,29 +1336,29 @@ const char *joypad_name(unsigned port) noexcept
|
||||
bool joypad_set_rumble(unsigned joypad, enum retro_rumble_effect effect,
|
||||
std::uint16_t strength) noexcept
|
||||
{
|
||||
if (joypad != 0 || !active_pad || active_pad->handle < 0)
|
||||
PadState *pad = pad_at(joypad);
|
||||
if (pad == nullptr || pad->handle < 0)
|
||||
return false;
|
||||
if (effect != RETRO_RUMBLE_STRONG && effect != RETRO_RUMBLE_WEAK)
|
||||
return false;
|
||||
pthread_mutex_lock(&active_pad->rumble_mutex);
|
||||
pthread_mutex_lock(&pad->rumble_mutex);
|
||||
const std::uint8_t level = static_cast<std::uint8_t>(strength >> 8);
|
||||
switch (effect)
|
||||
{
|
||||
case RETRO_RUMBLE_STRONG:
|
||||
active_pad->rumble_large.store(level, std::memory_order_relaxed);
|
||||
pad->rumble_large.store(level, std::memory_order_relaxed);
|
||||
break;
|
||||
case RETRO_RUMBLE_WEAK:
|
||||
active_pad->rumble_small.store(level, std::memory_order_relaxed);
|
||||
pad->rumble_small.store(level, std::memory_order_relaxed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const ScePadVibrationParam param{active_pad->rumble_large.load(std::memory_order_relaxed),
|
||||
active_pad->rumble_small.load(std::memory_order_relaxed)};
|
||||
const ScePadVibrationParam param{pad->rumble_large.load(std::memory_order_relaxed),
|
||||
pad->rumble_small.load(std::memory_order_relaxed)};
|
||||
// Advanced mode consumes the levels through PCM, not scePadSetVibration.
|
||||
const bool result =
|
||||
active_pad->haptic_active || scePadSetVibration(active_pad->handle, ¶m) == 0;
|
||||
pthread_mutex_unlock(&active_pad->rumble_mutex);
|
||||
const bool result = pad->haptic_active || scePadSetVibration(pad->handle, ¶m) == 0;
|
||||
pthread_mutex_unlock(&pad->rumble_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1278,8 +1447,9 @@ extern "C" input_driver_t input_ps5 = {
|
||||
// Reannounce once on the next poll, after all default settings have been applied.
|
||||
extern "C" void ps5_input_reset_autoconfig() noexcept
|
||||
{
|
||||
if (active_pad)
|
||||
active_pad->announced = false;
|
||||
for (PadState *state : pads)
|
||||
if (state != nullptr)
|
||||
state->announced = false;
|
||||
}
|
||||
|
||||
extern "C" input_device_driver_t ps5_joypad = {
|
||||
|
||||
@@ -426,6 +426,7 @@ int main(int process_argc, char **process_argv)
|
||||
static const char *const test_files[] = {
|
||||
"/app0/args.txt",
|
||||
"/app0/pad-script.txt",
|
||||
"/app0/pad-monitor.txt",
|
||||
"/app0/ps5vk-no-retain.txt",
|
||||
"/app0/ppsspp-options.txt",
|
||||
"/app0/ps5vk-ab.txt",
|
||||
|
||||
+148
-20
@@ -3,6 +3,9 @@
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
#include "../src/input_ps5.cpp"
|
||||
@@ -11,11 +14,26 @@
|
||||
static bool request_update_exit = false;
|
||||
namespace
|
||||
{
|
||||
std::vector<PadSample> pending;
|
||||
int read_result = 0;
|
||||
// Each user's controller has the user's id as its handle.
|
||||
std::map<int32_t, std::vector<PadSample>> pending;
|
||||
std::map<int32_t, int> read_results;
|
||||
unsigned reads = 0, opens = 0, closes = 0, connects = 0, disconnects = 0;
|
||||
unsigned port_connects[4] = {}, port_disconnects[4] = {};
|
||||
int32_t login_users[4] = {1, -1, -1, -1};
|
||||
int32_t refused_user = -1, held_user = -1;
|
||||
unsigned vibrations = 0;
|
||||
ScePadVibrationParam last_vibration{};
|
||||
int32_t last_vibration_handle = -1;
|
||||
std::mutex trace_mutex;
|
||||
std::vector<std::string> traces;
|
||||
unsigned traced(const char *text)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(trace_mutex);
|
||||
unsigned count = 0;
|
||||
for (const auto &line : traces)
|
||||
count += line.find(text) != std::string::npos;
|
||||
return count;
|
||||
}
|
||||
unsigned vibration_modes = 0;
|
||||
int32_t last_vibration_mode = -1;
|
||||
int accepted_format = 1, opened_format = -1;
|
||||
@@ -33,10 +51,10 @@ PadSample sample()
|
||||
p.timestamp_us = 10;
|
||||
return p;
|
||||
}
|
||||
void feed(PadSample p)
|
||||
void feed(PadSample p, int32_t handle = 1)
|
||||
{
|
||||
pending = {p};
|
||||
read_result = 1;
|
||||
pending[handle] = {p};
|
||||
read_results[handle] = 1;
|
||||
ps5_joypad.poll();
|
||||
}
|
||||
} // namespace
|
||||
@@ -46,10 +64,14 @@ extern "C"
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
int32_t scePadOpen(int32_t, int32_t, int32_t, const void *)
|
||||
int32_t scePadOpen(int32_t user, int32_t, int32_t, const void *)
|
||||
{
|
||||
++opens;
|
||||
return 1;
|
||||
return user == refused_user ? -2137784316 : user;
|
||||
}
|
||||
int32_t scePadGetHandle(int32_t user, int32_t, int32_t)
|
||||
{
|
||||
return user == held_user ? user : -1;
|
||||
}
|
||||
int32_t scePadClose(int32_t)
|
||||
{
|
||||
@@ -58,27 +80,29 @@ extern "C"
|
||||
}
|
||||
int32_t scePadSetVibration(int32_t handle, const ScePadVibrationParam *param)
|
||||
{
|
||||
assert(handle == 1 && param);
|
||||
assert(handle >= 1 && param);
|
||||
++vibrations;
|
||||
last_vibration = *param;
|
||||
last_vibration_handle = handle;
|
||||
return vibration_result;
|
||||
}
|
||||
int32_t scePadSetVibrationMode(int32_t handle, int32_t mode)
|
||||
{
|
||||
assert(handle == 1);
|
||||
assert(handle >= 1);
|
||||
++vibration_modes;
|
||||
last_vibration_mode = mode;
|
||||
return mode == 1 && fail_advanced ? -1 : 0;
|
||||
}
|
||||
int32_t scePadRead(int32_t, void *out, int32_t capacity)
|
||||
int32_t scePadRead(int32_t handle, void *out, int32_t capacity)
|
||||
{
|
||||
++reads;
|
||||
assert(capacity == 64);
|
||||
for (size_t i = 0; i < pending.size(); ++i)
|
||||
static_cast<PadSample *>(out)[i] = pending[i];
|
||||
pending.clear();
|
||||
int result = read_result;
|
||||
read_result = 0;
|
||||
auto &queue = pending[handle];
|
||||
for (size_t i = 0; i < queue.size(); ++i)
|
||||
static_cast<PadSample *>(out)[i] = queue[i];
|
||||
queue.clear();
|
||||
int result = read_results[handle];
|
||||
read_results[handle] = 0;
|
||||
return result;
|
||||
}
|
||||
int32_t sceUserServiceInitialize(const void *)
|
||||
@@ -90,6 +114,11 @@ extern "C"
|
||||
*user = 1;
|
||||
return 0;
|
||||
}
|
||||
int32_t sceUserServiceGetLoginUserIdList(SceUserServiceLoginUserIdList *list)
|
||||
{
|
||||
std::memcpy(list->user_id, login_users, sizeof(login_users));
|
||||
return 0;
|
||||
}
|
||||
int32_t sceUserServiceTerminate()
|
||||
{
|
||||
return 0;
|
||||
@@ -150,19 +179,23 @@ extern "C"
|
||||
{
|
||||
if (std::strstr(line, "back to motor rumble"))
|
||||
recovered = true;
|
||||
std::lock_guard<std::mutex> lock(trace_mutex);
|
||||
traces.emplace_back(line);
|
||||
}
|
||||
bool input_autoconfigure_connect(const char *name, const char *, const char *,
|
||||
const char *driver, unsigned port, unsigned, unsigned)
|
||||
{
|
||||
assert(std::strcmp(name, "PS5 Controller") == 0);
|
||||
assert(std::strcmp(driver, "ps5") == 0 && port == 0);
|
||||
assert(std::strcmp(driver, "ps5") == 0 && port < 4);
|
||||
++connects;
|
||||
++port_connects[port];
|
||||
return true;
|
||||
}
|
||||
bool input_autoconfigure_disconnect(unsigned port, const char *)
|
||||
{
|
||||
assert(port == 0);
|
||||
assert(port < 4);
|
||||
++disconnects;
|
||||
++port_disconnects[port];
|
||||
return true;
|
||||
}
|
||||
// The core options the script's OPTION action sets: one option, index 7,
|
||||
@@ -341,7 +374,7 @@ int main()
|
||||
assert(connects == 3 && opens == 1 && ps5_joypad.query_pad(0));
|
||||
ps5_joypad.poll();
|
||||
assert(connects == 3); // No per-frame announcement/task storm.
|
||||
read_result = -1;
|
||||
read_results[1] = -1;
|
||||
ps5_joypad.poll();
|
||||
assert(!ps5_joypad.query_pad(0) && mapped(RETRO_DEVICE_ID_JOYPAD_MASK) == 0);
|
||||
input_bits_t bits;
|
||||
@@ -402,7 +435,7 @@ int main()
|
||||
fail_advanced = failure == 1;
|
||||
assert(ps5_joypad.init(input));
|
||||
assert(last_vibration_mode == 2 && audio_closes == audio_opens);
|
||||
assert(!active_pad->haptic_thread_started && active_pad->haptic_port == -1);
|
||||
assert(!pads[0]->haptic_thread_started && pads[0]->haptic_port == -1);
|
||||
assert(ps5_joypad.set_rumble(0, RETRO_RUMBLE_STRONG, 65535));
|
||||
ps5_joypad.destroy();
|
||||
fail_thread = fail_advanced = false;
|
||||
@@ -416,6 +449,101 @@ int main()
|
||||
ps5_joypad.destroy();
|
||||
assert(!ps5_joypad.set_rumble(0, RETRO_RUMBLE_STRONG, 65535));
|
||||
|
||||
// Four players, as the console pairs controllers with signed-in users: port 0
|
||||
// is the user who started the title, a new user's controller takes the first
|
||||
// free port at the next scan, and a port is let go when its user signs out.
|
||||
{
|
||||
const auto scan = []
|
||||
{
|
||||
next_user_scan = std::chrono::steady_clock::time_point{};
|
||||
ps5_joypad.poll();
|
||||
};
|
||||
const unsigned opens_before = opens, closes_before = closes;
|
||||
assert(ps5_joypad.init(input));
|
||||
assert(opens == opens_before + 1 && pads[0] && pads[0]->user_id == 1 && !pads[1]);
|
||||
feed(sample());
|
||||
assert(ps5_joypad.query_pad(0) && !ps5_joypad.query_pad(1));
|
||||
assert(std::strcmp(ps5_joypad.name(0), "PS5 Controller") == 0 && !ps5_joypad.name(1));
|
||||
|
||||
login_users[1] = 5;
|
||||
login_users[2] = 3;
|
||||
scan();
|
||||
assert(opens == opens_before + 3 && pads[1] && pads[1]->user_id == 5 && pads[1]->port == 1);
|
||||
assert(pads[2] && pads[2]->user_id == 3 && !pads[3]);
|
||||
assert(traced("signed-in users 1 5 3 -1") == 1);
|
||||
// A port is announced once its controller reports itself connected.
|
||||
assert(!ps5_joypad.query_pad(1) && std::strcmp(ps5_joypad.name(1), "PS5 Controller") == 0);
|
||||
auto two = sample();
|
||||
two.buttons = pad_button_cross;
|
||||
feed(two, 5);
|
||||
assert(ps5_joypad.query_pad(1) && port_connects[1] == 1);
|
||||
assert(ps5_joypad.button(1, RETRO_DEVICE_ID_JOYPAD_B) &&
|
||||
!ps5_joypad.button(0, RETRO_DEVICE_ID_JOYPAD_B));
|
||||
auto three = sample();
|
||||
three.right_x = 255;
|
||||
feed(three, 3);
|
||||
assert(ps5_joypad.query_pad(2) && ps5_joypad.axis(2, AXIS_POS(2)) == 32767);
|
||||
assert(ps5_joypad.axis(0, AXIS_POS(2)) == 0 && ps5_joypad.axis(1, AXIS_POS(2)) == 0);
|
||||
// Each port rumbles its own controller.
|
||||
assert(ps5_joypad.set_rumble(1, RETRO_RUMBLE_STRONG, 65535) && last_vibration_handle == 5);
|
||||
assert(ps5_joypad.set_rumble(2, RETRO_RUMBLE_WEAK, 65535) && last_vibration_handle == 3);
|
||||
assert(!ps5_joypad.set_rumble(3, RETRO_RUMBLE_STRONG, 65535));
|
||||
|
||||
// User 5 signs out: port 1 is let go and announced gone; port 2 stays.
|
||||
login_users[1] = 3;
|
||||
login_users[2] = -1;
|
||||
const unsigned closes_mid = closes;
|
||||
scan();
|
||||
assert(!pads[1] && pads[2] && pads[2]->user_id == 3 && port_disconnects[1] == 1);
|
||||
assert(closes == closes_mid + 1 && !ps5_joypad.query_pad(1) &&
|
||||
!ps5_joypad.button(1, RETRO_DEVICE_ID_JOYPAD_B));
|
||||
// A controller that will not open is skipped, traced once, and tried again;
|
||||
// then the new user takes the first free port.
|
||||
login_users[2] = 7;
|
||||
refused_user = 7;
|
||||
scan();
|
||||
scan();
|
||||
assert(!pads[1] && traced("port 1: scePadOpen for user 7 failed") == 1);
|
||||
refused_user = -1;
|
||||
scan();
|
||||
assert(pads[1] && pads[1]->user_id == 7 && port_connects[1] == 1);
|
||||
// The user who started the title keeps port 0 after signing out.
|
||||
login_users[0] = 3;
|
||||
login_users[1] = 7;
|
||||
login_users[2] = 9;
|
||||
login_users[3] = -1;
|
||||
scan();
|
||||
assert(pads[0] && pads[0]->user_id == 1 && pads[3] && pads[3]->user_id == 9);
|
||||
// A full house: every port is taken, so a fourth signed-in user besides the
|
||||
// three has none, and is not even tried.
|
||||
login_users[3] = 11;
|
||||
const unsigned opens_full = opens;
|
||||
scan();
|
||||
assert(opens == opens_full);
|
||||
for (unsigned port = 0; port < 4; ++port)
|
||||
assert(pads[port] && pads[port]->user_id != 11);
|
||||
// The pad monitor traces each port's buttons when they change.
|
||||
pad_monitor = true;
|
||||
two.buttons = pad_button_circle;
|
||||
feed(two, 7);
|
||||
feed(two, 7);
|
||||
assert(traced("monitor port 1 user 7 buttons 0x00002000") == 1);
|
||||
two.buttons = pad_button_intercepted;
|
||||
feed(two, 7);
|
||||
assert(traced("buttons 0x80000000 (the system has it)") == 1 &&
|
||||
!ps5_joypad.button(1, RETRO_DEVICE_ID_JOYPAD_A));
|
||||
pad_monitor = false;
|
||||
// A config reset re-announces every connected port.
|
||||
const unsigned connects_before = connects;
|
||||
ps5_input_reset_autoconfig();
|
||||
ps5_joypad.poll();
|
||||
assert(connects == connects_before + 3); // ports 0, 1 and 2 had reported in
|
||||
ps5_joypad.destroy();
|
||||
assert(closes == closes_before + 5 && !pads[0] && !pads[3]); // user 5, then all four
|
||||
login_users[0] = 1;
|
||||
login_users[1] = login_users[2] = login_users[3] = -1;
|
||||
}
|
||||
|
||||
// STOP ends the run on the next frame, as --max-frames does, and only once.
|
||||
test_video.frame_count = 1234;
|
||||
actions[0] = ScriptAction{0.0, false, ScriptActionKind::stop, -1, {}, {}};
|
||||
@@ -455,7 +583,7 @@ int main()
|
||||
assert(test_runloop.max_frames == 51); // repeated polls cannot postpone exit
|
||||
request_update_exit = false;
|
||||
std::puts("PS5 joypad: raw binding capture, axes, user mappings, rumble, poll retention, "
|
||||
"lifecycle and the script's STOP and OPTION PASS");
|
||||
"lifecycle, four players by signed-in user and the script's STOP and OPTION PASS");
|
||||
}
|
||||
|
||||
namespace ps5_update
|
||||
|
||||
+12
-2
@@ -30,6 +30,9 @@
|
||||
# tools/run-title.sh --pad-script=tests/pad/manual-scan-reentry.txt --watch 40
|
||||
# RetroArch with the pad scripted (src/input_ps5.cpp says
|
||||
# how); its SCREENSHOT pictures are collected afterwards
|
||||
# tools/run-title.sh --pad-monitor --watch 120
|
||||
# trace each controller port's buttons as they change, to
|
||||
# tell several controllers apart in the trace
|
||||
#
|
||||
# Why this exists. Every earlier round of the console loop was four hand-driven
|
||||
# steps that needed a person: build, upload, launch, read. Two things went wrong
|
||||
@@ -72,6 +75,7 @@ frontend_scroll=
|
||||
picker_test=
|
||||
retroarch_frames=0
|
||||
pad_script=
|
||||
pad_monitor=0
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--no-build) build=0 ;;
|
||||
@@ -94,7 +98,8 @@ while (( $# )); do
|
||||
--picker-test=*) picker_test=${1#*=} ;;
|
||||
--retroarch-frames=*) retroarch_frames=${1#*=} ;;
|
||||
--pad-script=*) pad_script=${1#*=} ;;
|
||||
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test[=fceumm|mgba|snes9x|fbneo|genesis_plus_gx|ppsspp]] [--relaunch-test[=1..20] [--relaunch-image=PATH]] [--display-modes-test[=frames]] [--frontend-capture=SECONDS[,SECONDS...] [--frontend-scroll] [--frontend-profile] [--frontend-quit] [--frontend-launch]] [--picker-test=FRAMES:retroarch|es-de|none] [--retroarch-frames=N] [--pad-script=FILE]" >&2; exit 2 ;;
|
||||
--pad-monitor) pad_monitor=1 ;;
|
||||
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test[=fceumm|mgba|snes9x|fbneo|genesis_plus_gx|ppsspp]] [--relaunch-test[=1..20] [--relaunch-image=PATH]] [--display-modes-test[=frames]] [--frontend-capture=SECONDS[,SECONDS...] [--frontend-scroll] [--frontend-profile] [--frontend-quit] [--frontend-launch]] [--picker-test=FRAMES:retroarch|es-de|none] [--retroarch-frames=N] [--pad-script=FILE] [--pad-monitor]" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
@@ -249,7 +254,7 @@ fi
|
||||
# appeared. A run that does not ask for extras must not inherit them, so the file
|
||||
# is removed on every run - before the launch, because deleting it afterwards
|
||||
# would leave it for the next one if this run dies.
|
||||
python3 - "$title_id" "$profile" "$audio_test" "$core_test" "$relaunch_test" "$relaunch_run" "$display_modes_test" "$relaunch_image" "$frontend_capture" "$capture_run" "$frontend_scroll" "$picker_test" "$retroarch_frames" "$pad_script" <<'PY'
|
||||
python3 - "$title_id" "$profile" "$audio_test" "$core_test" "$relaunch_test" "$relaunch_run" "$display_modes_test" "$relaunch_image" "$frontend_capture" "$capture_run" "$frontend_scroll" "$picker_test" "$retroarch_frames" "$pad_script" "$pad_monitor" <<'PY'
|
||||
import importlib.util, sys
|
||||
spec = importlib.util.spec_from_file_location("dt", "tools/deploy-title.py")
|
||||
dt = importlib.util.module_from_spec(spec); spec.loader.exec_module(dt)
|
||||
@@ -337,6 +342,11 @@ with connect(**dt.load_settings()) as ftp:
|
||||
with open(sys.argv[14], "rb") as script:
|
||||
ftp.storbinary(f"STOR /data/homebrew/{sys.argv[1]}/pad-script.txt", script)
|
||||
print(f" armed pad script {sys.argv[14]}")
|
||||
# The pad monitor (src/input_ps5.cpp): each controller port's buttons traced.
|
||||
remove_if_present(ftp, f"/data/homebrew/{sys.argv[1]}/pad-monitor.txt")
|
||||
if int(sys.argv[15]):
|
||||
ftp.storbinary(f"STOR /data/homebrew/{sys.argv[1]}/pad-monitor.txt", io.BytesIO(b"pad monitor\n"))
|
||||
print(" armed the pad monitor: each controller port's buttons go to the trace")
|
||||
# The launch is a test run's (src/main.cpp): it keeps the test files this run
|
||||
# armed, and starts RetroArch rather than the frontend picker
|
||||
# (src/frontend_mode_ps5.cpp) unless a picker test is armed.
|
||||
|
||||
Reference in new issue
Block a user