Files
PS5_RetroArch/src/input_ps5.cpp
T

925 lines
35 KiB
C++

/*
* PS5 RetroArch - the console's gamepad, as RetroArch's input driver.
*
* Copyright (C) 2026 Mihawk
* SPDX-License-Identifier: GPL-3.0-or-later
*
* The joypad interface owns the native pad. RetroArch polls it for binding
* discovery, menu stick navigation and mapped core input. The input interface
* deliberately reports no additional gamepad state: duplicating raw buttons there
* would OR the old hardcoded mapping back into user-configured bindings.
*
* The ABI is not derivable and is not guessed. `scePadInit`, `scePadOpen`,
* `scePadRead` and the 120-byte sample layout below were verified on hardware by
* ../ProsperoLight, a native PS5 title whose controls work: its src/radio_input.cpp
* and src/moonlight_stream.cpp carry the same calls, the same button bits and the
* same offsets, including `connected` at 0x4c and the timestamp at 0x50. The static
* assertions are what keeps a wrong layout from compiling quietly.
*
* 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
* UP DOWN LEFT RIGHT A X L R L2 R2 L3 R3, and LS_ or RS_ with UP DOWN LEFT or RIGHT
* for a stick pushed all the way) and `#` comments. The pressed buttons
* are added to the pad's own, and the pad counts as connected while a script is
* loaded, so RetroArch binds it. One trace line a press. One line is an action
* rather than a press, for the reload stress (Profile 9): `<seconds> RELOAD`
* loads /app0/args.txt's core and content again in the same process, as the
* menu's history does. It is meant for the menu, after the script closed the
* content through the Quick Menu: the poll runs inside the core's retro_run
* while content runs, and unloading a core from there crashed Dolphin inside
* its own frame (2026-09-25), which is why there is no CLOSE action. And
* `<seconds> STOP` ends the run as --max-frames does, with its screenshot:
* RetroArch forgets --max-frames when a core closes, so a run that reloads
* content otherwise never ends or takes its screenshot. `<seconds> SAVE_STATE`
* and `<seconds> LOAD_STATE` save and load the current slot, as the menu's
* items do, for a run that starts from a known scene; a number after either
* names another slot for that action only. `<seconds> SCREENSHOT`
* saves the frame shown then as /app0/pad-shot-<n>.png (n counts from 1), so a
* run that walks through a game's screens shows each of them; it is encoded on
* RetroArch's task thread, so the game runs on (a 4K PNG held the frontend's own
* thread about 20 s). `<seconds> MARK`
* prints how many new frames the core has made and the longest gap between
* two since the last MARK, so a run's frame rate is read over exact script
* times. Testing only: the file is never shipped.
*
* Reference: docs/REFERENCE.md, "Input".
*/
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <new>
#include <gfx/video_defines.h>
#include <input/input_driver.h>
#include "content.h"
#ifdef HAVE_MENU
#include "command.h"
#include "configuration.h"
#include "core_option_manager.h"
#endif
#include "gfx/video_driver.h"
#include "retroarch_types.h"
#include "runloop.h"
#include "tasks/task_content.h"
#include <tasks/tasks_internal.h>
extern "C"
{
/* The console's pad service. Declarations rather than the SDK's headers: this
* payload SDK ships no header for these, and ../ProsperoLight declares the same
* shapes, which the console accepted. */
std::int32_t scePadInit();
std::int32_t scePadOpen(std::int32_t user_id, std::int32_t port_type, std::int32_t index,
const void *params);
std::int32_t scePadRead(std::int32_t handle, void *samples, std::int32_t capacity);
std::int32_t scePadClose(std::int32_t handle);
std::int32_t sceUserServiceInitialize(const void *params);
std::int32_t sceUserServiceGetInitialUser(std::int32_t *user_id);
std::int32_t sceUserServiceTerminate();
std::int32_t sceKernelUsleep(std::uint32_t microseconds);
}
namespace
{
/* The console's pad words. Verified on hardware by ../ProsperoLight; each is a bit
* of what is held down, not an index. */
constexpr std::uint32_t pad_button_l3 = 0x000002u;
constexpr std::uint32_t pad_button_r3 = 0x000004u;
constexpr std::uint32_t pad_button_options = 0x000008u;
constexpr std::uint32_t pad_button_up = 0x000010u;
constexpr std::uint32_t pad_button_right = 0x000020u;
constexpr std::uint32_t pad_button_down = 0x000040u;
constexpr std::uint32_t pad_button_left = 0x000080u;
constexpr std::uint32_t pad_button_l1 = 0x000400u;
constexpr std::uint32_t pad_button_r1 = 0x000800u;
constexpr std::uint32_t pad_button_triangle = 0x001000u;
constexpr std::uint32_t pad_button_circle = 0x002000u;
constexpr std::uint32_t pad_button_cross = 0x004000u;
constexpr std::uint32_t pad_button_square = 0x008000u;
constexpr std::uint32_t pad_button_touch_pad = 0x100000u;
/* Set while the shell is intercepting the pad; no button means anything then. */
constexpr std::uint32_t pad_button_intercepted = UINT32_C(0x80000000);
/* One sample, as the console writes it. */
struct PadSample
{
std::uint32_t buttons;
std::uint8_t left_x;
std::uint8_t left_y;
std::uint8_t right_x;
std::uint8_t right_y;
std::uint8_t left_trigger;
std::uint8_t right_trigger;
std::uint8_t reserved_to_connected[66];
std::int32_t connected;
std::uint64_t timestamp_us;
std::uint8_t extension[16];
std::uint8_t connected_count;
std::uint8_t remaining[15];
};
static_assert(sizeof(PadSample) == 120, "the console's pad samples are 120 bytes");
static_assert(offsetof(PadSample, left_x) == 0x04, "the stick bytes follow the button word");
static_assert(offsetof(PadSample, connected) == 0x4c, "connection state sits at 0x4c");
static_assert(offsetof(PadSample, timestamp_us) == 0x50, "the timestamp sits at 0x50");
/* One read returns a batch of the samples taken since the last one. */
constexpr int sample_capacity = 64;
constexpr std::int32_t pad_open_attempts = 10;
constexpr std::uint32_t pad_open_retry_microseconds = 100000;
/* The stick bytes run 0..255 with 128 centred. */
constexpr int stick_centre = 128;
/* The trace is a development aid: one line goes to the title's own file, which is
* the only output this project has that survives a run. It lives in src/trace.cpp,
* whose signature is C++ and therefore not reachable from an `extern "C"`
* declaration - the driver calls through this small C-linkage door instead. */
} // namespace
extern "C" void ps5_input_trace(const char *line) noexcept;
namespace
{
struct PadState
{
std::int32_t handle = -1;
PadSample samples[sample_capacity];
/* How many entries of `samples` the last read actually filled. A read returns
* only what happened since the previous one, so the rest of the buffer still
* holds older frames and must not be reported as current. */
std::int32_t sample_count = 0;
std::uint32_t buttons = 0;
bool owns_user_service = false;
bool announced = false;
};
PadState *active_pad = nullptr;
/* The pad script (see the top of this file). */
struct ScriptPress
{
double at;
double until;
std::uint32_t mask;
bool announced;
};
constexpr int script_capacity = 128;
constexpr double script_default_hold = 0.15;
ScriptPress script[script_capacity];
int script_count = 0;
/* The script's actions (the top of this file), run once each from the poll on
* the main thread. */
enum class ScriptActionKind
{
reload,
stop,
save_state,
load_state,
screenshot,
mark,
option,
};
// The core's new frames (gfx/video_driver.c, patches/series 0101), the
// longest time between two of them since the last MARK and how many came more
// than 40 ms after the one before (a 30 fps frame that missed its slot on a
// 60 Hz display), which a MARK line prints: a run's frame rate, worst frame
// and stutters over exact script times.
std::atomic<std::uint64_t> new_frames{0};
std::atomic<std::uint64_t> new_frame_worst_ns{0};
std::atomic<std::uint64_t> new_frames_slow{0};
std::uint64_t new_frame_last_ns = 0;
struct ScriptAction
{
double at;
bool done;
ScriptActionKind kind;
int slot; // SAVE_STATE and LOAD_STATE: the slot given, or -1 for the current one
char key[64]; // OPTION: the core option's key
char value[32]; // OPTION: the value to set, as the core lists it
};
constexpr int action_capacity = 32;
ScriptAction actions[action_capacity];
int action_count = 0;
int screenshot_count = 0;
bool script_started = false;
std::chrono::steady_clock::time_point script_start;
/* A script's stick directions follow the 16 RetroPad buttons in its masks, two
* a stick axis (left X, left Y, right X, right Y), the negative one first. */
constexpr unsigned script_stick_shift = 16;
std::uint32_t retropad_button(const char *name) noexcept
{
static const char *const names[24] = {
"B", "Y", "SELECT", "START", "UP", "DOWN", "LEFT", "RIGHT",
"A", "X", "L", "R", "L2", "R2", "L3", "R3",
"LS_LEFT", "LS_RIGHT", "LS_UP", "LS_DOWN", "RS_LEFT", "RS_RIGHT", "RS_UP", "RS_DOWN"};
for (unsigned i = 0; i < 24; ++i)
if (std::strcmp(name, names[i]) == 0)
return UINT32_C(1) << i;
return 0;
}
void load_script() noexcept
{
std::FILE *file = std::fopen("/app0/pad-script.txt", "rb");
if (file == nullptr)
return;
char line[160];
while (script_count < script_capacity && std::fgets(line, sizeof(line), file) != nullptr)
{
char *hash = std::strchr(line, '#');
if (hash != nullptr)
*hash = '\0';
double at = 0.0;
double held = script_default_hold;
char buttons[96] = {0};
const int fields = std::sscanf(line, "%lf %95s %lf", &at, buttons, &held);
if (fields < 2)
continue;
static const struct
{
const char *name;
ScriptActionKind kind;
} action_names[] = {{"RELOAD", ScriptActionKind::reload},
{"STOP", ScriptActionKind::stop},
{"SAVE_STATE", ScriptActionKind::save_state},
{"LOAD_STATE", ScriptActionKind::load_state},
{"SCREENSHOT", ScriptActionKind::screenshot},
{"MARK", ScriptActionKind::mark}};
// OPTION key value: set a core option as the Quick Menu does (the core
// sees it as an update at its next frame)
if (std::strcmp(buttons, "OPTION") == 0 && action_count < action_capacity)
{
ScriptAction option{at, false, ScriptActionKind::option, -1, {0}, {0}};
if (std::sscanf(line, "%lf %*s %63s %31s", &at, option.key, option.value) == 3)
actions[action_count++] = option;
continue;
}
bool is_action = false;
for (const auto &named : action_names)
if (std::strcmp(buttons, named.name) == 0 && action_count < action_capacity)
{
actions[action_count++] = ScriptAction{
at, false, named.kind, fields >= 3 ? static_cast<int>(held) : -1, {0}, {0}};
is_action = true;
}
if (is_action)
continue;
std::uint32_t mask = 0;
for (char *name = std::strtok(buttons, "+"); name != nullptr;
name = std::strtok(nullptr, "+"))
mask |= retropad_button(name);
if (mask != 0)
script[script_count++] = ScriptPress{at, at + held, mask, false};
}
std::fclose(file);
char note[96];
std::snprintf(note, sizeof(note), "input: pad script loaded, %d presses", script_count);
ps5_input_trace(note);
}
/* The scripted buttons held now, as RetroPad bits. */
std::uint32_t script_buttons() noexcept
{
if (script_count == 0)
return 0;
const auto now = std::chrono::steady_clock::now();
if (!script_started)
{
script_started = true;
script_start = now;
}
const double seconds = std::chrono::duration<double>(now - script_start).count();
std::uint32_t mask = 0;
for (int i = 0; i < script_count; ++i)
{
ScriptPress &press = script[i];
if (seconds < press.at || seconds >= press.until)
continue;
mask |= press.mask;
if (!press.announced)
{
press.announced = true;
char note[96];
std::snprintf(note, sizeof(note), "input: pad script press %d at %.2f s: 0x%04x", i,
seconds, static_cast<unsigned>(press.mask));
ps5_input_trace(note);
}
}
return mask;
}
PadState *state_of(void *data) noexcept
{
return static_cast<PadState *>(data);
}
/* The newest of the samples the last read filled, or null when the pad is not
* there or the shell is intercepting it. */
const PadSample *newest_sample(const PadState &state) noexcept
{
const PadSample *newest = nullptr;
for (std::int32_t index = 0; index < state.sample_count; ++index)
{
const PadSample &sample = state.samples[index];
if (newest == nullptr || sample.timestamp_us > newest->timestamp_us)
newest = &sample;
}
if (newest == nullptr || !newest->connected || (newest->buttons & pad_button_intercepted) != 0)
return nullptr;
return newest;
}
/* The pad's held buttons as RetroArch's own button indices, as a bitmask.
*
* The two orderings do not correspond: the console numbers its buttons by position
* around the shell and RetroArch numbers them by the position they held on a Super
* Nintendo pad - A rightmost, B bottom, X top, Y left. CIRCLE is therefore
* RetroArch's A and CROSS is its B, which is what makes CIRCLE confirm a menu entry
* and CROSS cancel it, the pairing a PlayStation player expects. */
std::uint32_t pad_buttons_to_retropad(std::uint32_t pad) noexcept
{
std::uint32_t mask = 0;
if (pad & pad_button_up)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_UP;
if (pad & pad_button_down)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_DOWN;
if (pad & pad_button_left)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_LEFT;
if (pad & pad_button_right)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_RIGHT;
if (pad & pad_button_circle)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_A;
if (pad & pad_button_cross)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_B;
if (pad & pad_button_triangle)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_X;
if (pad & pad_button_square)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_Y;
if (pad & pad_button_l1)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_L;
if (pad & pad_button_r1)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_R;
if (pad & pad_button_l3)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_L3;
if (pad & pad_button_r3)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_R3;
if (pad & pad_button_options)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_START;
if (pad & pad_button_touch_pad)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_SELECT;
return mask;
}
/* A scripted stick direction's full deflection on a stick axis (0 to 3), or 0;
* on the trigger axes (4 and 5, which the profile binds L2 and R2 to), a
* scripted L2 or R2 pulls the trigger fully. */
int script_axis(unsigned index) noexcept
{
if (index == 4 || index == 5)
return script_buttons() & (UINT32_C(1) << (index == 4 ? RETRO_DEVICE_ID_JOYPAD_L2
: RETRO_DEVICE_ID_JOYPAD_R2))
? 32767
: 0;
if (index > 3)
return 0;
const std::uint32_t held = script_buttons() >> (script_stick_shift + index * 2);
return (held & 2) ? 32767 : (held & 1) ? -32768 : 0;
}
/* A stick byte as a signed 16-bit axis. */
std::int16_t stick_axis(std::uint8_t value) noexcept
{
const int offset = static_cast<int>(value) - stick_centre;
int scaled = offset * 32767 / (offset < 0 ? 128 : 127);
if (scaled > 32767)
scaled = 32767;
if (scaled < -32768)
scaled = -32768;
return static_cast<std::int16_t>(scaled);
}
void *open_pad() noexcept
{
auto *state = new (std::nothrow) PadState();
if (state == nullptr)
{
ps5_input_trace("input: the driver 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;
}
/* 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)
{
state->handle = scePadOpen(user_id, 0, 0, nullptr);
if (state->handle >= 0)
break;
(void)sceKernelUsleep(pad_open_retry_microseconds);
}
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;
}
{
char line[176];
std::snprintf(line, sizeof(line), "input: pad opened, user=%d handle=%d",
static_cast<int>(user_id), state->handle);
ps5_input_trace(line);
}
return state;
}
void poll_pad(void *data) noexcept
{
PadState *state = state_of(data);
if (state == nullptr || state->handle < 0)
return;
const std::int32_t count = scePadRead(state->handle, state->samples, sample_capacity);
if (count == 0)
return; // No new samples: preserve the last state across a second binding poll.
if (count < 0 || count > sample_capacity)
{
/* The service refused the read or the pad went away. Nothing is reported
* rather than the last state being repeated, so a button cannot stick
* down. */
state->sample_count = 0;
state->buttons = 0;
return;
}
state->sample_count = count;
const PadSample *newest = newest_sample(*state);
state->buttons = newest != nullptr ? newest->buttons : 0;
/* Successful button transitions are routine, not diagnostics. Keep pad-open
* failures and lifecycle logs, without a synchronous file write per press. */
}
void close_pad(void *data) noexcept
{
PadState *state = state_of(data);
if (state == nullptr)
return;
if (state->handle >= 0)
{
(void)scePadClose(state->handle);
state->handle = -1;
}
if (state->owns_user_service)
{
(void)sceUserServiceTerminate();
state->owns_user_service = false;
}
delete state;
}
void *joypad_init(void *) noexcept
{
if (!active_pad)
{
active_pad = state_of(open_pad());
if (active_pad && script_count == 0)
load_script();
}
if (active_pad)
ps5_input_trace("input: ps5 joypad registered (16 buttons, 6 axes)");
return active_pad;
}
void joypad_destroy() noexcept
{
close_pad(active_pad);
active_pad = nullptr;
}
/* The core and content /app0/args.txt names: the line after -L, and the first
* line that is not an option. */
bool args_paths(char *core, std::size_t core_size, char *content, std::size_t content_size) noexcept
{
std::FILE *file = std::fopen("/app0/args.txt", "rb");
if (file == nullptr)
return false;
char line[512];
bool next_is_core = false;
core[0] = content[0] = '\0';
while (std::fgets(line, sizeof(line), file) != nullptr)
{
line[std::strcspn(line, "\r\n")] = '\0';
if (next_is_core)
{
std::snprintf(core, core_size, "%s", line);
next_is_core = false;
}
else if (std::strcmp(line, "-L") == 0)
next_is_core = true;
else if (line[0] != '-' && line[0] != '\0' && content[0] == '\0')
std::snprintf(content, content_size, "%s", line);
}
std::fclose(file);
return core[0] != '\0' && content[0] != '\0';
}
/* Runs the script's actions whose time has come. */
void run_script_actions() noexcept
{
if (action_count == 0)
return;
if (!script_started)
{
script_started = true;
script_start = std::chrono::steady_clock::now();
}
const double seconds =
std::chrono::duration<double>(std::chrono::steady_clock::now() - script_start).count();
for (int i = 0; i < action_count; ++i)
{
ScriptAction &action = actions[i];
if (action.done || seconds < action.at)
continue;
action.done = true;
if (action.kind == ScriptActionKind::save_state ||
action.kind == ScriptActionKind::load_state)
{
const bool save = action.kind == ScriptActionKind::save_state;
#ifdef HAVE_MENU
// A slot given names the file for this action only (the path is
// made from the slot when the command runs)
settings_t *settings = config_get_ptr();
const int slot = settings->ints.state_slot;
if (action.slot >= 0)
settings->ints.state_slot = action.slot;
const bool ok =
command_event(save ? CMD_EVENT_SAVE_STATE : CMD_EVENT_LOAD_STATE, nullptr);
settings->ints.state_slot = slot;
#else
const bool ok = false;
#endif
char note[96];
std::snprintf(note, sizeof(note), "input: pad script %s %d at %.2f s: %d",
save ? "SAVE_STATE" : "LOAD_STATE", action.slot, seconds, ok ? 1 : 0);
ps5_input_trace(note);
continue;
}
if (action.kind == ScriptActionKind::screenshot)
{
char path[64];
std::snprintf(path, sizeof(path), "/app0/pad-shot-%d.png", ++screenshot_count);
#ifdef HAVE_SCREENSHOTS
const video_driver_state_t *video_st = video_state_get_ptr();
const bool ok =
take_screenshot(nullptr, path, false,
video_st->frame_cache_data &&
video_st->frame_cache_data == RETRO_HW_FRAME_BUFFER_VALID,
true, true);
#else
const bool ok = false;
#endif
char note[128];
std::snprintf(note, sizeof(note), "input: pad script SCREENSHOT at %.2f s: %s %d",
seconds, path, ok ? 1 : 0);
ps5_input_trace(note);
continue;
}
if (action.kind == ScriptActionKind::option)
{
runloop_state_t *runloop = runloop_state_get_ptr();
size_t idx = 0;
size_t val_idx = 0;
const bool ok =
runloop->core_options &&
core_option_manager_get_idx(runloop->core_options, action.key, &idx) &&
core_option_manager_get_val_idx(runloop->core_options, idx, action.value, &val_idx);
if (ok)
core_option_manager_set_val(runloop->core_options, idx, val_idx, false);
char note[160];
std::snprintf(note, sizeof(note), "input: pad script OPTION %s=%s at %.2f s: %d",
action.key, action.value, seconds, ok ? 1 : 0);
ps5_input_trace(note);
continue;
}
if (action.kind == ScriptActionKind::mark)
{
const std::uint64_t worst_ns =
new_frame_worst_ns.exchange(0, std::memory_order_relaxed);
char note[128];
std::snprintf(
note, sizeof(note),
"input: pad script MARK at %.3f s: frames=%llu worst_ms=%.1f over_40ms=%llu",
seconds,
static_cast<unsigned long long>(new_frames.load(std::memory_order_relaxed)),
static_cast<double>(worst_ns) / 1e6,
static_cast<unsigned long long>(new_frames_slow.load(std::memory_order_relaxed)));
ps5_input_trace(note);
continue;
}
if (action.kind == ScriptActionKind::stop)
{
// The next frame is the last, as when --max-frames is reached.
runloop_state_get_ptr()->max_frames =
static_cast<unsigned>(video_state_get_ptr()->frame_count + 1);
char note[96];
std::snprintf(note, sizeof(note), "input: pad script STOP at %.2f s", seconds);
ps5_input_trace(note);
continue;
}
char core[256];
char content[256];
bool pushed = false;
#ifdef HAVE_MENU
if (args_paths(core, sizeof(core), content, sizeof(content)))
{
content_ctx_info_t info{};
pushed = task_push_load_content_with_new_core_from_menu(
core, content, &info, CORE_TYPE_PLAIN, nullptr, nullptr);
}
#else
(void)core;
(void)content;
#endif
char note[160];
std::snprintf(note, sizeof(note), "input: pad script RELOAD at %.2f s: %d", seconds,
pushed ? 1 : 0);
ps5_input_trace(note);
}
}
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)
{
active_pad->announced = connected;
if (connected)
input_autoconfigure_connect("PS5 Controller", nullptr, nullptr, "ps5", 0, 0, 0);
else
input_autoconfigure_disconnect(0, "PS5 Controller");
}
}
bool joypad_query(unsigned port) noexcept
{
return port == 0 && active_pad && active_pad->announced;
}
std::uint32_t joypad_buttons(unsigned port) noexcept
{
if (port != 0 || !active_pad)
return 0;
const std::uint32_t scripted = script_buttons() & 0xffff;
const PadSample *sample = newest_sample(*active_pad);
if (!sample)
return scripted;
auto mask = pad_buttons_to_retropad(sample->buttons) | scripted;
if (sample->left_trigger > 127)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_L2;
if (sample->right_trigger > 127)
mask |= UINT32_C(1) << RETRO_DEVICE_ID_JOYPAD_R2;
return mask;
}
std::int32_t joypad_button(unsigned port, std::uint16_t key) noexcept
{
return key < 16 && (joypad_buttons(port) & (UINT32_C(1) << key)) ? 1 : 0;
}
void joypad_get_buttons(unsigned port, input_bits_t *bits) noexcept
{
BIT256_CLEAR_ALL_PTR(bits);
BITS_COPY16_PTR(bits, joypad_buttons(port));
}
std::int16_t joypad_axis(unsigned port, std::uint32_t axis) noexcept
{
if (port != 0 || !active_pad || 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);
if (value != 0)
;
else if (!sample)
return 0;
else
switch (index)
{
case 0:
value = stick_axis(sample->left_x);
break;
case 1:
value = stick_axis(sample->left_y);
break;
case 2:
value = stick_axis(sample->right_x);
break;
case 3:
value = stick_axis(sample->right_y);
break;
case 4:
value = sample->left_trigger * 32767 / 255;
break;
case 5:
value = sample->right_trigger * 32767 / 255;
break;
default:
return 0;
}
return negative ? (value < 0 ? value : 0) : (value > 0 ? value : 0);
}
std::int16_t joypad_state(rarch_joypad_info_t *info, const retro_keybind *binds, unsigned) noexcept
{
if (!info || !binds)
return 0;
std::uint16_t mask = 0;
for (unsigned i = 0; i < RARCH_FIRST_CUSTOM_BIND; ++i)
{
if (!binds[i].valid)
continue;
const auto key = binds[i].joykey != NO_BTN
? binds[i].joykey
: (info->auto_binds ? info->auto_binds[i].joykey : NO_BTN);
const auto axis = binds[i].joyaxis != AXIS_NONE
? binds[i].joyaxis
: (info->auto_binds ? info->auto_binds[i].joyaxis : AXIS_NONE);
if (joypad_button(info->joy_idx, key) ||
std::abs(int(joypad_axis(info->joy_idx, axis))) / 32768.0f > info->axis_threshold)
mask |= UINT16_C(1) << i;
}
return static_cast<std::int16_t>(mask);
}
const char *joypad_name(unsigned port) noexcept
{
return port == 0 ? "PS5 Controller" : nullptr;
}
void *ps5_input_init(const char *) noexcept
{
static int cookie;
return &cookie;
}
void ps5_input_poll(void *) noexcept
{
}
void ps5_input_free(void *) noexcept
{
}
std::int16_t ps5_input_state(void *, const input_device_driver_t *, const input_device_driver_t *,
rarch_joypad_info_t *, const retro_keybind_set *, bool, unsigned,
unsigned, unsigned, unsigned) noexcept
{
return 0; // Mapped joypad input is supplied by RetroArch's input_state_wrap.
}
std::uint64_t ps5_input_capabilities(void *data) noexcept
{
(void)data;
return (UINT64_C(1) << RETRO_DEVICE_JOYPAD) | (UINT64_C(1) << RETRO_DEVICE_ANALOG);
}
} // namespace
/* The pairing between RetroArch's button numbering and the console's own words,
* exposed as data so the host test can pin it without a console. The two orderings
* do not correspond, and reading one as the other is a fault whose only symptom is
* the wrong button moving the menu. */
extern "C" const std::uint32_t *ps5_input_button_map(std::size_t *entries) noexcept
{
static const std::uint32_t map[] = {
RETRO_DEVICE_ID_JOYPAD_UP, pad_button_up,
RETRO_DEVICE_ID_JOYPAD_DOWN, pad_button_down,
RETRO_DEVICE_ID_JOYPAD_LEFT, pad_button_left,
RETRO_DEVICE_ID_JOYPAD_RIGHT, pad_button_right,
RETRO_DEVICE_ID_JOYPAD_B, pad_button_cross,
RETRO_DEVICE_ID_JOYPAD_A, pad_button_circle,
RETRO_DEVICE_ID_JOYPAD_Y, pad_button_square,
RETRO_DEVICE_ID_JOYPAD_X, pad_button_triangle,
RETRO_DEVICE_ID_JOYPAD_L, pad_button_l1,
RETRO_DEVICE_ID_JOYPAD_R, pad_button_r1,
RETRO_DEVICE_ID_JOYPAD_L3, pad_button_l3,
RETRO_DEVICE_ID_JOYPAD_R3, pad_button_r3,
RETRO_DEVICE_ID_JOYPAD_START, pad_button_options,
RETRO_DEVICE_ID_JOYPAD_SELECT, pad_button_touch_pad,
};
if (entries != nullptr)
*entries = sizeof(map) / sizeof(map[0]) / 2;
return map;
}
/* The driver table. Positions are the interface: see `struct input_driver` in
* input/input_driver.h. It ends at keypress_vibrate, so the literal is the whole
* table. C linkage and a global name, because input/input_driver.c is C and names
* this symbol in input_drivers[]. */
extern "C" input_driver_t input_ps5 = {
ps5_input_init,
ps5_input_poll,
ps5_input_state,
ps5_input_free,
nullptr, /* set_sensor_state */
nullptr, /* get_sensor_input */
ps5_input_capabilities,
"ps5",
nullptr, /* grab_mouse */
nullptr, /* grab_stdin */
nullptr, /* keypress_vibrate */
};
// Defaults clear RetroArch's auto-binds without recreating the pad driver.
// 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;
}
extern "C" input_device_driver_t ps5_joypad = {
joypad_init, joypad_query, joypad_destroy, joypad_button, joypad_state, joypad_get_buttons,
joypad_axis, joypad_poll, nullptr, nullptr, nullptr, nullptr,
joypad_name, "ps5",
};
// Built into RetroArch's autoconfiguration list, so existing saved configs with
// an empty joypad driver work without replacing the owner's settings.
extern "C" const char ps5_controller_profile[] = "input_device = \"PS5 Controller\"\n"
"input_driver = \"ps5\"\n"
"input_b_btn = \"0\"\n"
"input_y_btn = \"1\"\n"
"input_select_btn = \"2\"\n"
"input_start_btn = \"3\"\n"
"input_up_btn = \"4\"\n"
"input_down_btn = \"5\"\n"
"input_left_btn = \"6\"\n"
"input_right_btn = \"7\"\n"
"input_a_btn = \"8\"\n"
"input_x_btn = \"9\"\n"
"input_l_btn = \"10\"\n"
"input_r_btn = \"11\"\n"
"input_l3_btn = \"14\"\n"
"input_r3_btn = \"15\"\n"
"input_l_x_plus_axis = \"+0\"\n"
"input_l_x_minus_axis = \"-0\"\n"
"input_l_y_plus_axis = \"+1\"\n"
"input_l_y_minus_axis = \"-1\"\n"
"input_r_x_plus_axis = \"+2\"\n"
"input_r_x_minus_axis = \"-2\"\n"
"input_r_y_plus_axis = \"+3\"\n"
"input_r_y_minus_axis = \"-3\"\n"
"input_l2_axis = \"+4\"\n"
"input_r2_axis = \"+5\"\n";
// Called by RetroArch's video driver for each frame the core made (patches/series,
// 0101), on the thread that runs frames.
extern "C" void ps5_input_note_new_frame(void)
{
const std::uint64_t now =
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
if (new_frame_last_ns != 0 &&
now - new_frame_last_ns > new_frame_worst_ns.load(std::memory_order_relaxed))
new_frame_worst_ns.store(now - new_frame_last_ns, std::memory_order_relaxed);
if (new_frame_last_ns != 0 && now - new_frame_last_ns > 40'000'000)
new_frames_slow.fetch_add(1, std::memory_order_relaxed);
new_frame_last_ns = now;
new_frames.fetch_add(1, std::memory_order_relaxed);
}