refactor(pad): trim comments and apply naming conventions

Bring the pad-gamepad-haptics diff in line with docs/dev/CONVENTIONS.md:
strip explanatory/rationale comments (struct layouts, algorithm notes,
rewired-NID rationale) down to code only, and rename the anonymous-
namespace helpers in PadState.cpp (Normalize/Fuse/EncodeZones/MaxOf/
ScaleStrength) to camelCase to match upstream's free-function style.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
OverkillLabs 2026-09-29 04:24:39 +03:00
parent 9673bce423
commit e532b19cee
7 changed files with 57 additions and 112 deletions

View File

@ -591,7 +591,6 @@ struct Ngs2WaveformFormat {
};
struct Ngs2WaveformBlock {
// PS5 grew the offset/size pair to 64 bits: the record is 40 bytes, not the 32-byte PS4 layout.
std::uint64_t data_offset;
std::uint64_t data_size;
std::uint32_t num_repeats;

View File

@ -22,7 +22,6 @@ int APS5_VABI scePadClose_nid_postfix(int handle) {
int APS5_VABI scePadDeviceClassGetExtendedInformation(int handle, PadDeviceClassExtendedInformation* info) {
if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE;
if (info == nullptr) return PAD_ERROR_INVALID_ARG;
// Standard digital pad: device class only, every class-specific field zero.
std::memset(info, 0, sizeof(*info));
info->deviceClass = PAD_DEVICE_CLASS_STANDARD;
return PAD_OK;
@ -33,7 +32,7 @@ int APS5_VABI scePadDeviceClassParseData(int handle, const PadData* data, PadDev
if (data == nullptr || class_data == nullptr) return PAD_ERROR_INVALID_ARG;
std::memset(class_data, 0, sizeof(*class_data));
class_data->deviceClass = PAD_DEVICE_CLASS_STANDARD;
class_data->dataValid = data->connected; // no steering/guitar/drum payload for a standard pad
class_data->dataValid = data->connected;
return PAD_OK;
}
@ -65,13 +64,12 @@ int APS5_VABI scePadGetHandle(int user_id, int type, int index) {
return 0;
}
// Port 0 holds the virtual DualSense while the host has no pad open; fields per the PadInfo layout.
int APS5_VABI scePadGetInfo_nid_postfix(PadInfo* info) {
if (info == nullptr) return PAD_ERROR_INVALID_ARG;
std::memset(info, 0, sizeof(*info));
info->maxConnectCount = 4;
info->connectedCount[0] = 1; // standard ports
info->connectedCount[1] = 0; // special ports
info->connectedCount[0] = 1;
info->connectedCount[1] = 0;
PadInfo::PadTypeInfo& slot = info->padTypeInfo[0];
slot.connectPort = 0;
slot.status = 1;
@ -84,7 +82,6 @@ int APS5_VABI scePadGetInfo_nid_postfix(PadInfo* info) {
int APS5_VABI scePadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) {
if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE;
if (info == nullptr) return PAD_ERROR_INVALID_ARG;
// Both triggers report the neutral "no feedback engaged" state; the host pad has no trigger readback.
std::memset(info, 0, sizeof(*info));
return PAD_OK;
}
@ -92,7 +89,6 @@ int APS5_VABI scePadGetTriggerEffectState(int handle, PadTriggerEffectStateInfor
int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) {
if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE;
if (info == nullptr) return PAD_ERROR_INVALID_ARG;
// ScePadExtendedControllerInformation: base information followed by the class extension (all zero for a standard pad).
std::memset(info, 0, 0x2c);
PadControllerInformation* base = static_cast<PadControllerInformation*>(info);
base->touchPadInfo.pixelDensity = 44.86f;
@ -173,7 +169,6 @@ int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) {
return 0;
}
// ScePadLightBarParam: u8 r, g, b at +0..2.
int APS5_VABI scePadSetLightBar(int handle, const PadLightBarParam* param) {
if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE;
if (param == nullptr) return PAD_ERROR_INVALID_ARG;
@ -194,10 +189,6 @@ int APS5_VABI scePadSetTiltCorrectionState(int handle, bool enabled) {
return 0;
}
// ScePadTriggerEffectParam: u8 triggerMask (bit 0 = L2, bit 1 = R2), u8 padding[7], ScePadTriggerEffectCommandData command[2] (56 bytes each).
// Not one of the extended-info NIDs the audit called out by name, but wired to a real implementation here
// because it is the guest entry point for the adaptive-trigger plumbing added alongside it (Pad::SetTriggerCommand);
// leaving it NotImplemented would make the trigger effect state/output machinery unreachable from the guest.
int APS5_VABI scePadSetTriggerEffect(int handle, const void* param) {
if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE;
if (param == nullptr) return PAD_ERROR_INVALID_ARG;
@ -208,7 +199,6 @@ int APS5_VABI scePadSetTriggerEffect(int handle, const void* param) {
}
std::uint32_t mode[2] = {};
for (int trigger = 0; trigger < 2; ++trigger) std::memcpy(&mode[trigger], bytes + 8 + 56 * trigger, 4);
// Modes 0..6 are the SDK ScePadTriggerEffectMode values; anything else means the struct was misread.
if (mode[0] > 6 || mode[1] > 6) {
return PAD_ERROR_INVALID_ARG;
}
@ -218,10 +208,6 @@ int APS5_VABI scePadSetTriggerEffect(int handle, const void* param) {
return PAD_OK;
}
// ScePadVibrationParam: u8 largeMotor at +0, u8 smallMotor at +1 (vibration-mode motors only; the trigger
// motors are driven through scePadSetTriggerEffect).
// Also not in the audit's named list, but wired here for the same reason as scePadSetTriggerEffect: it is
// the only guest entry point for Pad::SetVibration, which the rumble plumbing added alongside it needs.
int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) {
if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE;
if (param == nullptr) return PAD_ERROR_INVALID_ARG;
@ -229,8 +215,6 @@ int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) {
return PAD_OK;
}
// mode: 0 = desktop (USB) vibration layout, 1 = embedded controller layout. Both drive the same host rumble.
// Same rationale as scePadSetVibration: the only guest entry point for Pad::SetVibrationMode.
int APS5_VABI scePadSetVibrationMode(int handle, int mode) {
if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE;
if (mode != 0 && mode != 1) return PAD_ERROR_INVALID_ARG;

View File

@ -16,36 +16,30 @@ constexpr int PAD_HANDLE = 1;
constexpr int PAD_CONNECTION_TYPE_LOCAL = 0;
constexpr int PAD_DEVICE_CLASS_STANDARD = 0;
// ScePadDeviceType values reconstructed from DualSense HID telemetry; no current title queries them,
// they only describe the virtual device.
constexpr int PAD_DEVICE_TYPE_NONE = 0;
constexpr int PAD_DEVICE_TYPE_NAVIGATOR = 1;
constexpr int PAD_DEVICE_TYPE_DUAL_SHOCK_4 = 2;
constexpr int PAD_DEVICE_TYPE_DUAL_SENSE = 3;
constexpr int PAD_DEVICE_TYPE_DUAL_SENSE_EDGE = 4;
// ScePadConnectType values from the PS4/PS5 SDK.
constexpr int PAD_CONNECT_TYPE_UNREGISTERED = 0;
constexpr int PAD_CONNECT_TYPE_USB = 1;
constexpr int PAD_CONNECT_TYPE_BLUETOOTH = 2;
// Bluetooth public address of the virtual pad; DC:8C:37 is the Sony OUI a retail DualSense uses.
inline constexpr std::uint8_t PAD_BD_ADDRESS[6] = {0xDC, 0x8C, 0x37, 0x51, 0x01, 0xF4};
// Mirrors scePadGetInfo's ScePadInfo: per-port slot table of the pad service. The exact SDK field names
// are not public; this is the layout every out-param filler in this library writes and documents.
struct PadInfo {
std::uint8_t maxConnectCount; // 0x00
std::uint8_t connectedCount[2]; // 0x01 per personal port type (standard, special)
std::uint8_t reserved0[2]; // 0x03
std::uint8_t maxConnectCount;
std::uint8_t connectedCount[2];
std::uint8_t reserved0[2];
struct PadTypeInfo {
std::uint8_t connectPort; // index within the port type
std::uint8_t status; // 0 free, 1 connected
std::uint8_t deviceType; // PAD_DEVICE_TYPE_*
std::uint8_t connectType; // PAD_CONNECT_TYPE_*
std::uint8_t bluetoothMacAddress[6]; // empty unless status == 1
} padTypeInfo[8]; // 0x05, 10 bytes per slot
std::uint8_t reserved1[4]; // 0x55
std::uint8_t connectPort;
std::uint8_t status;
std::uint8_t deviceType;
std::uint8_t connectType;
std::uint8_t bluetoothMacAddress[6];
} padTypeInfo[8];
std::uint8_t reserved1[4];
};
static_assert(sizeof(PadInfo) == 0x59);

View File

@ -8,8 +8,8 @@
struct PadTouchPoint {
bool active = false;
std::uint16_t x = 0; // 0..1919
std::uint16_t y = 0; // 0..942
std::uint16_t x = 0;
std::uint16_t y = 0;
std::uint8_t id = 0;
bool operator==(const PadTouchPoint&) const = default;
@ -22,32 +22,30 @@ struct PadInputState {
std::uint8_t analogButtonsR2 = 0;
bool touchLeft = false;
bool touchRight = false;
// Host gamepad extras (all neutral when no gamepad is present).
bool hasMotion = false; // host pad delivers accelerometer + gyroscope
std::array<float, 3> accel{0.0f, 9.80665f, 0.0f}; // m/s^2, +Y up when the pad lies flat
std::array<float, 3> gyro{0.0f, 0.0f, 0.0f}; // rad/s
std::array<PadTouchPoint, 2> touch{}; // host touchpad fingers
std::uint8_t deviceKind = 0; // 0 none, 1 DualSense, 2 DualShock 4, 3 other pad
bool hasMotion = false;
std::array<float, 3> accel{0.0f, 9.80665f, 0.0f};
std::array<float, 3> gyro{0.0f, 0.0f, 0.0f};
std::array<PadTouchPoint, 2> touch{};
std::uint8_t deviceKind = 0;
};
// Output requests issued by the guest (rumble, light bar, adaptive triggers), applied to the host pad by the window thread.
struct PadTriggerRequest {
bool valid = false; // an effect other than "off" is engaged
std::uint8_t effect[11]{}; // DualSense native trigger effect block
std::uint8_t fallback = 0; // 0..255 rumble strength for pads without adaptive triggers
std::uint32_t mode = 0; // guest ScePadTriggerEffectMode
bool valid = false;
std::uint8_t effect[11]{};
std::uint8_t fallback = 0;
std::uint32_t mode = 0;
};
struct PadOutputState {
std::uint32_t sequence = 0; // bumped on every change
std::uint32_t sequence = 0;
std::uint8_t vibrationLarge = 0;
std::uint8_t vibrationSmall = 0;
bool lightBarValid = false; // false: default colour
bool lightBarValid = false;
std::uint8_t lightBar[3]{};
PadTriggerRequest trigger[2]; // 0 = L2, 1 = R2
bool triggerTouched = false; // an effect struct was received at least once
PadTriggerRequest trigger[2];
bool triggerTouched = false;
bool motionEnabled = true;
int vibrationMode = 0; // last ScePadSetVibrationMode argument (0/r0 = desktop, 1/r1 = embedded)
int vibrationMode = 0;
};
namespace Pad {
@ -56,7 +54,6 @@ PadData ReadState();
void SetVibration(std::uint8_t large, std::uint8_t small);
void SetVibrationMode(int mode);
void SetLightBar(bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b);
// trigger: 0 = L2, 1 = R2; command is a ScePadTriggerEffectCommandData (56 bytes: u32 mode, padding, command data at +8).
void SetTriggerCommand(int trigger, const std::uint8_t* command);
void ResetOrientation();
void SetMotionEnabled(bool enabled);
@ -64,7 +61,6 @@ void SetMotionEnabled(bool enabled);
extern "C" void PadPublishInput_nid_postfix(const PadInputState& input);
extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error);
// Copies the current output request when it differs from *seenSequence, updating it; returns true when a copy was made.
extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t* seenSequence, PadOutputState* out);
#endif

View File

@ -15,8 +15,6 @@ namespace {
std::exception_ptr failure;
bool initialized = false;
// Orientation fusion: gyro integration with accelerometer tilt correction (Mahony filter). The identity
// quaternion is the pad lying flat and face up, which is also what a pad without sensors reports.
struct Quat { float w = 1.0f, x = 0.0f, y = 0.0f, z = 0.0f; };
Quat orientation;
std::uint64_t lastFuseTime = 0;
@ -25,18 +23,17 @@ namespace {
bool prevTouchActive[2] = {false, false};
std::uint8_t touchIds[2] = {0, 0};
void Normalize(Quat& q) {
void normalize(Quat& q) {
const float n = std::sqrt(q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z);
if (n < 1e-6f) { q = Quat{}; return; }
q.w /= n; q.x /= n; q.y /= n; q.z /= n;
}
void Fuse(float dt, const std::array<float, 3>& accel, const std::array<float, 3>& gyro) {
void fuse(float dt, const std::array<float, 3>& accel, const std::array<float, 3>& gyro) {
float gx = gyro[0], gy = gyro[1], gz = gyro[2];
const float an = std::sqrt(accel[0] * accel[0] + accel[1] * accel[1] + accel[2] * accel[2]);
if (an > 1.0f) {
const float ax = accel[0] / an, ay = accel[1] / an, az = accel[2] / an;
// Gravity direction predicted by the current orientation, in the body frame (rest pose: +Y).
const float vx = 2.0f * (orientation.x * orientation.y + orientation.w * orientation.z);
const float vy = orientation.w * orientation.w - orientation.x * orientation.x + orientation.y * orientation.y - orientation.z * orientation.z;
const float vz = 2.0f * (orientation.y * orientation.z - orientation.w * orientation.x);
@ -58,12 +55,10 @@ namespace {
orientation.x = q.x + q.w * hx + q.y * hz - q.z * hy;
orientation.y = q.y + q.w * hy - q.x * hz + q.z * hx;
orientation.z = q.z + q.w * hz + q.x * hy - q.y * hx;
Normalize(orientation);
normalize(orientation);
}
// DualSense native effect block: byte 0 = mode, [1..2] = active zone mask, [3..6] = 3-bit strength per zone,
// [9] = frequency (vibration modes). Mode 0x05 means "no effect".
void EncodeZones(std::uint8_t* dst, std::uint8_t mode, const std::uint8_t* strengths, int count, std::uint8_t frequency) {
void encodeZones(std::uint8_t* dst, std::uint8_t mode, const std::uint8_t* strengths, int count, std::uint8_t frequency) {
std::uint16_t zones = 0;
std::uint32_t packed = 0;
for (int i = 0; i < count && i < 10; ++i) {
@ -80,13 +75,13 @@ namespace {
dst[9] = frequency;
}
std::uint8_t MaxOf(const std::uint8_t* v, int n) {
std::uint8_t maxOf(const std::uint8_t* v, int n) {
std::uint8_t m = 0;
for (int i = 0; i < n; ++i) m = std::max(m, v[i]);
return m;
}
std::uint8_t ScaleStrength(std::uint8_t s) {
std::uint8_t scaleStrength(std::uint8_t s) {
return s == 0 ? 0 : static_cast<std::uint8_t>(std::min(255, 32 * std::min<int>(s, 8)));
}
}
@ -115,8 +110,6 @@ PadData Pad::ReadState() {
data.analog_buttons_l2 = std::max<std::uint8_t>(state.analogButtonsL2, (state.buttons & 0x100) != 0 ? 255 : 0);
data.analog_buttons_r2 = std::max<std::uint8_t>(state.analogButtonsR2, (state.buttons & 0x200) != 0 ? 255 : 0);
// Motion: real sensor data when the host pad has it and the guest enabled the sensors, otherwise the pad at rest
// (gravity on +Y, no rotation, identity orientation).
const bool live = state.hasMotion && output.motionEnabled;
const std::array<float, 3> rest{0.0f, 9.80665f, 0.0f};
const std::array<float, 3>& accel = live ? state.accel : rest;
@ -124,7 +117,7 @@ PadData Pad::ReadState() {
if (live) {
float dt = lastFuseTime != 0 && now > lastFuseTime ? static_cast<float>(now - lastFuseTime) * 1e-6f : 0.0f;
dt = std::min(dt, 0.1f);
Fuse(dt, accel, gyro);
fuse(dt, accel, gyro);
} else {
orientation = Quat{};
biasIntegral[0] = biasIntegral[1] = biasIntegral[2] = 0.0f;
@ -134,8 +127,6 @@ PadData Pad::ReadState() {
data.orientation_y = orientation.y;
data.orientation_z = orientation.z;
data.orientation_w = orientation.w;
// ScePadData reports acceleration in units of g (rest = 1.0 on the "up" axis), not m/s^2; the fusion
// math above stays in m/s^2 because Fuse()'s free-fall gate assumes that scale.
constexpr float kGravity = 9.80665f;
data.acceleration_x = accel[0] / kGravity;
data.acceleration_y = accel[1] / kGravity;
@ -148,7 +139,6 @@ PadData Pad::ReadState() {
data.connected_count = 1;
data.timestamp = timestamp;
// Touch: host touchpad fingers, plus the keyboard emulation (BACKSPACE / TAB) when no finger is down.
std::uint8_t touchNum = 0;
for (int i = 0; i < 2; ++i) {
const PadTouchPoint& tp = state.touch[i];
@ -206,14 +196,14 @@ void Pad::SetTriggerCommand(int trigger, const std::uint8_t* command) {
req.mode = mode;
req.effect[0] = 0x05;
switch (mode) {
case 1: { // feedback { position, strength }
case 1: {
std::uint8_t strengths[10] = {};
if (p[0] <= 9 && p[1] >= 1 && p[1] <= 8) for (int i = p[0]; i < 10; ++i) strengths[i] = p[1];
EncodeZones(req.effect, 0x21, strengths, 10, 0);
req.fallback = ScaleStrength(p[1]);
encodeZones(req.effect, 0x21, strengths, 10, 0);
req.fallback = scaleStrength(p[1]);
break;
}
case 2: { // weapon { startPosition, endPosition, strength }
case 2: {
if (p[0] >= 2 && p[0] <= 7 && p[1] > p[0] && p[1] <= 8 && p[2] >= 1 && p[2] <= 8) {
const std::uint16_t zones = static_cast<std::uint16_t>((1u << p[0]) | (1u << p[1]));
req.effect[0] = 0x25;
@ -221,36 +211,36 @@ void Pad::SetTriggerCommand(int trigger, const std::uint8_t* command) {
req.effect[2] = static_cast<std::uint8_t>(zones >> 8);
req.effect[3] = static_cast<std::uint8_t>(p[2] - 1);
}
req.fallback = ScaleStrength(p[2]);
req.fallback = scaleStrength(p[2]);
break;
}
case 3: { // vibration { position, amplitude, frequency }
case 3: {
std::uint8_t strengths[10] = {};
if (p[0] <= 9 && p[1] >= 1 && p[1] <= 8) for (int i = p[0]; i < 10; ++i) strengths[i] = p[1];
EncodeZones(req.effect, 0x26, strengths, 10, p[2]);
req.fallback = ScaleStrength(p[1]);
encodeZones(req.effect, 0x26, strengths, 10, p[2]);
req.fallback = scaleStrength(p[1]);
break;
}
case 4: // multi-position feedback { strength[10] }
EncodeZones(req.effect, 0x21, p, 10, 0);
req.fallback = ScaleStrength(MaxOf(p, 10));
case 4:
encodeZones(req.effect, 0x21, p, 10, 0);
req.fallback = scaleStrength(maxOf(p, 10));
break;
case 5: { // slope feedback { startPosition, endPosition, startStrength, endStrength }
case 5: {
std::uint8_t strengths[10] = {};
if (p[0] <= 8 && p[1] > p[0] && p[1] <= 9 && p[2] >= 1 && p[2] <= 8 && p[3] >= 1 && p[3] <= 8) {
const int dist = p[1] - p[0];
for (int i = p[0]; i < 10; ++i)
strengths[i] = i <= p[1] ? static_cast<std::uint8_t>(std::lround(p[2] + (p[3] - p[2]) * (i - p[0]) / static_cast<double>(dist))) : p[3];
}
EncodeZones(req.effect, 0x21, strengths, 10, 0);
req.fallback = ScaleStrength(std::max(p[2], p[3]));
encodeZones(req.effect, 0x21, strengths, 10, 0);
req.fallback = scaleStrength(std::max(p[2], p[3]));
break;
}
case 6: // multi-position vibration { frequency, amplitude[10] }
EncodeZones(req.effect, 0x26, p + 1, 10, p[0]);
req.fallback = p[0] == 0 ? 0 : ScaleStrength(MaxOf(p + 1, 10));
case 6:
encodeZones(req.effect, 0x26, p + 1, 10, p[0]);
req.fallback = p[0] == 0 ? 0 : scaleStrength(maxOf(p + 1, 10));
break;
default: break; // 0 = off
default: break;
}
req.valid = req.effect[0] != 0x05;
if (!req.valid) req.fallback = 0;
@ -276,9 +266,6 @@ void Pad::SetMotionEnabled(bool enabled) {
extern "C" void PadPublishInput_nid_postfix(const PadInputState& input) {
std::lock_guard lock(stateMutex);
if (failure) std::rethrow_exception(failure);
// Change-detection kept from upstream, extended to the motion/touch fields: a pad with live sensors
// changes on essentially every call (so this is a no-op there), while a pad without one still only
// bumps the reported timestamp when something actually changed.
if (state.buttons == input.buttons && state.sticks == input.sticks &&
state.analogButtonsL2 == input.analogButtonsL2 && state.analogButtonsR2 == input.analogButtonsR2 &&
state.touchLeft == input.touchLeft && state.touchRight == input.touchRight &&

View File

@ -34,10 +34,8 @@ private:
std::array<std::uint8_t, 2> mouseStick{128, 128};
std::chrono::steady_clock::time_point nextMousePoll{};
bool mouseEnabled = false;
// Host gamepad (Xbox / DualShock / DualSense / Switch Pro ... through SDL's GameController mapping database), merged with keyboard and mouse.
SDL_GameController* controller = nullptr;
PadInputState controllerState{};
// Guest output requests (rumble, light bar, adaptive triggers) mirrored onto the host pad.
std::uint32_t outputSequence = 0;
PadOutputState outputState{};
bool outputPending = false;

View File

@ -24,13 +24,11 @@ PadInput::~PadInput() {
void PadInput::openFirstAvailableController() {
if (controller != nullptr) return;
// Read continuously (not just when the SDL hint is checked at subsystem init), so this is set every
// time regardless of whether the video driver already brought SDL_INIT_GAMECONTROLLER up.
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); // keep reading the pad while the window is unfocused
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
if ((SDL_WasInit(SDL_INIT_GAMECONTROLLER) & SDL_INIT_GAMECONTROLLER) == 0) {
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
APS5_LOG_ERR("Pad: SDL game controller init failed: %s", SDL_GetError());
return; // no controller support: keyboard and mouse keep working
return;
}
}
for (int deviceIndex = 0; deviceIndex < SDL_NumJoysticks(); ++deviceIndex) {
@ -53,7 +51,7 @@ void PadInput::openController(int deviceIndex) {
SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE, SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE,
SDL_GameControllerGetNumTouchpads(controller), SDL_GameControllerHasLED(controller) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE);
enableSensors();
outputPending = true; // re-apply the current guest output request to the newly opened pad
outputPending = true;
}
void PadInput::enableSensors() {
@ -70,7 +68,6 @@ void PadInput::closeController() {
controllerState = {};
}
// Pushes the rumble / light bar / trigger requests of the guest to the host pad. Runs on the window thread that owns the SDL joystick.
void PadInput::applyOutput() {
PadOutputState fetched;
if (PadFetchOutput_nid_postfix(&outputSequence, &fetched)) {
@ -85,7 +82,6 @@ void PadInput::applyOutput() {
const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0;
const bool isPs5 = SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_PS5;
if (!outputPending) {
// Rumble requests expire host-side after a fixed time; keep them alive while the guest holds them.
if ((rumbling || (triggerRumble && !isPs5)) && now >= nextRumbleRefresh) outputPending = true;
else return;
}
@ -99,7 +95,6 @@ void PadInput::applyOutput() {
}
if (outputState.triggerTouched) {
if (isPs5) {
// DS5EffectsState: enable bits 0x04 (right) / 0x08 (left) at byte 0, right effect block at +10, left effect block at +21 (47 bytes).
Uint8 effect[47] = {};
effect[0] = 0x04 | 0x08;
std::memcpy(effect + 10, outputState.trigger[1].effect, 11);
@ -121,14 +116,9 @@ void PadInput::setMouseMode(bool enabled) {
nextMousePoll = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::MousePollIntervalMs);
}
// SDL reports buttons by physical position (A = bottom face button), which on a PlayStation layout is Cross, so the mapping below is the
// PlayStation one for every pad type; a Switch-style layout is normalised by SDL's own mapping database.
PadInputState PadInput::sampleController() const {
PadInputState result;
if (controller == nullptr) return result;
// Button/trigger/stick mapping and scaling kept identical to the pre-existing gamepad support
// (SDL_GameControllerGetAttached block previously inlined in publish()); only motion, touchpad
// finger tracking, and device-kind detection are new here.
const auto readButton = [this](SDL_GameControllerButton button) {
return SDL_GameControllerGetButton(controller, button) != 0;
};
@ -267,7 +257,6 @@ void PadInput::Update() {
if (controller != nullptr) SDL_GameControllerUpdate();
applyOutput();
if (controller != nullptr) {
// Sensors change on every sample, so a pad with motion republishes continuously.
controllerState = sampleController();
publish();
}
@ -304,8 +293,6 @@ void PadInput::Update() {
}
void PadInput::publish() {
// Gamepad is the base layer (like a title reading a real DualSense); keyboard and mouse bindings are
// applied on top and win when they conflict with the gamepad, matching the pre-existing keyboard/mouse behaviour.
PadInputState state;
state.buttons = controllerState.buttons;
state.sticks = controllerState.sticks;