Status: draft v1 · 2026-09-27 · synced with main 2026-10-01
Guest controller input through libScePad, plus the guest keyboard and mouse libraries (libSceMouse, libSceKeyboard). The host side covers:
- XInput controllers;
- DualSense over USB;
- keyboard and mouse mapped onto a virtual pad;
- hot-plug and user-to-pad assignment.
Out of scope for 1.0 (PRD §5):
- DualSense haptics and adaptive triggers (rumble, light bar, motion and touchpad are in scope; adaptive trigger forwarding exists as best effort and is unverified, see "DualSense output and motion"; speaker and actuator audio is an AudioOut2 matter, bean
portps5-ds7a); - Bluetooth-specific DualSense features;
- touchpad gestures beyond a click.
PRD bar owned here: F5 (XInput, DualSense USB, keyboard/mouse mapping).
References are relative to the AnyPS5 tree and cite main@75a8668 (current AnyPS5 main, includes merged PR #5) unless marked main@e06dbff (the pre-merge baseline).
| Area | Verified state |
|---|---|
| Physical controllers | main@e06dbff: none. It references no SDL_GameController, SDL_Joystick or XInput anywhere under core/, and the top-level CMakeLists.txt:26-28 forces SDL_JOYSTICK, SDL_HAPTIC and SDL_HIDAPI off, and :29 forces SDL_SENSOR off. main@75a8668: SDL game controllers are opened, hot-plugged and sampled through PadInput (libSceVideoOut/src/PadInput.cpp:25-69, :119-188, :190-208), and CMakeLists.txt:28 turns SDL_JOYSTICK on. SDL_HAPTIC, SDL_HIDAPI and SDL_SENSOR stay off (:29-31). XInput is not referenced anywhere. |
| Keyboard/mouse → pad | A default compile-time table of 33 bindings (core/libs/prx/libScePad/include/InputMapping.hpp:18-52): WASD for the left stick, TFGH for the right stick, mouse-left = Square, mouse-right = R2, wheel = D-pad up/down, middle-click toggles mouse-look, F11 toggles fullscreen. Mouse-look polls every 33 ms at a fixed sensitivity (:14-16). main@75a8668 can override the table from an anyps5-input.ini next to the executable or from the path in ANYPS5_INPUT_CONFIG (libSceVideoOut/src/InputMapping.cpp:128-146). |
| Event plumbing | Events are read in the VideoOut window loop (libSceVideoOut/src/VideoOutDriver.cpp:483-490), then PadInput::HandleEvent, Update and publish run (libSceVideoOut/src/PadInput.cpp:190-342). APS5_NO_PAD_INPUT drops keys, mouse buttons and the wheel (PadInput.cpp:216-219, :237). |
scePadOpen |
Accepts index 0 only and returns a constant handle (libScePad/Export.cpp:121-132). |
scePadRead |
main@e06dbff throws (Export.cpp:90-96). main@75a8668 returns one current state per call (Export.cpp:136-142). |
| Pad state | Always reports connected, and connectedCount is 1 (Export.cpp:39-57; src/PadState.cpp:138-139). L2/R2 analog values come from the controller's analog triggers, with 255 when only the digital bit is set (PadState.cpp:110-111). Motion is the controller's accelerometer and gyro when motion is enabled and present, otherwise a constant rest pose (:113-136). The timestamp advances only when the published state changes (:266-276). |
| Output features | main@75a8668 forwards scePadSetVibration, SetLightBar, ResetLightBar, SetTriggerEffect, SetMotionSensorState and ResetOrientation to PadInput::applyOutput (Export.cpp:153-163, :172-183, :192-216; PadInput.cpp:71-107). GetHandle, SetTiltCorrectionState, SetAngularVelocityDeadbandState and SetVibrationTriggerEffectWeakWhileEmbeddedMicInUse still throw (Export.cpp:59-65, :165-170, :185-190, :225-229). main@e06dbff throws for all of them. |
libSceMouse, libSceKeyboard |
libSceKeyboard: every export throws (libSceKeyboard/Export.cpp:8-52). libSceMouse: every export throws at main@e06dbff (Export.cpp:8-32), while main@75a8668 implements sceMouseInit, Open, Close and Read over src/mouse_impl.cpp (Export.cpp:5-21). PortPS5's libSceMouse/Export.cpp still throws (see M1). |
| Known debt | docs/TechnicalDebt.md:33 (PortPS5): there is no way to change the keyboard/mouse mapping. main@75a8668 has the .ini override above. |
the decision table in README.md says "libScePad implements scePadRead over SDL". At main@e06dbff that is accurate only for keyboard and mouse events. main@75a8668 also reads SDL game controllers, but XInput is not referenced, and SDL_HIDAPI, SDL_HAPTIC and SDL_SENSOR are still forced off (CMakeLists.txt:29-31), so how far XInput and DualSense USB support goes is unverified here and stays new PortPS5 work.
Status as of 2026-10-01 (PortPS5 main). The table above cites AnyPS5 trees. On PortPS5 main: SDL_JOYSTICK and SDL_HIDAPI are ON (CMakeLists.txt:55-58; SDL_HAPTIC and SDL_SENSOR stay off); SDL game controllers feed scePadRead with hot-plug, per-slot connect and a radial dead zone (PRs #54 and #57, see "Controller polling (implemented)" below; not verified with a physical device); libSceMouse keeps a backend and VideoOut routing, but every sceMouse* export is an Unsupported() abort (libSceMouse/Export.cpp, bean portps5-afme); libSceKeyboard's 6 exports are Unsupported() aborts (libSceKeyboard/Export.cpp). Added in the DualSense lane (bean portps5-j7ds, see "DualSense output and motion"): per-slot output queue and window-thread forwarding to SDL (rumble, light bar, DS5 trigger effects), motion and orientation, touchpad fingers, a pure SlotTable for hot-plug, and real implementations of the previously aborting scePadResetOrientation, scePadSetTiltCorrectionState, scePadGetTriggerEffectState and scePadDeviceClass* exports. None of it is verified with a physical controller (bean portps5-ds7h). Open: TOML bindings, XInput and DualSense manual matrix (bean portps5-de24).
This follows the decision table in README.md §Input:
- Keep SDL and extend it.
- Add a keyboard/mouse mapping and bindings table in TOML.
- Add hot-plug and controller-number assignment.
- DualSense haptics and adaptive triggers are post-1.0.
- Build. (Done:
CMakeLists.txt:55-58.) Turn onSDL_JOYSTICKandSDL_HIDAPI. LeaveSDL_HAPTICandSDL_SENSORoff in 1.0. Pin an SDL2 revision whose HIDAPI driver supports PS5 controllers. Inference: that arrived in SDL 2.0.14. The pinned submodule commit4b69833must be checked against it (build-toolchain.md). - Device layer. An
InputHubowns SDLGameControllerinstances and receivesCONTROLLERDEVICEADDED/REMOVEDevents from the existing window loop.- XInput devices come through SDL's XInput/RawInput backends.
- DualSense over USB comes through HIDAPI (
SDL_HINT_JOYSTICK_HIDAPI_PS5=1). Full-report mode is enabled only when a later feature needs it.
- Pad slots. Up to 4 slots map to the logged-in users that
libSceUserServicereports. 1.0 has one user (see save-data.md).- The first controller that connects takes slot 0.
- Keyboard/mouse is always an extra source merged into slot 0.
- A disconnected controller frees its slot. The guest sees
connected = falseuntil a controller reconnects, and it is then reassigned to the same slot if it is the same device GUID.
- State model. Each slot keeps real analog sticks and triggers, buttons, and
connectedCountincremented on every reconnect. The timestamp is refreshed on every host poll, not only on change, so titles that detect a stale timestamp see a live pad. Guest dead zones come fromGetControllerInformation. The host applies no extra dead zone beyond an 8% radial default, which is configurable. - Mapping sources, merged per slot:
- Controller: SDL canonical layout to the pad layout (South = Cross, East = Circle, and so on).
- Keyboard/mouse: the TOML bindings table. The current
InputMapping.hpptable becomes the shipped default inconfig/global.toml.
- TOML schema (the
[input]section; validation rules are in configuration.md):
| Key | Type | Default | Meaning |
|---|---|---|---|
deadzone |
float 0.0–0.5 | 0.08 | Host radial dead zone for sticks. |
mouse_look |
bool | false | Start with mouse-look on. The middle button still toggles it. |
mouse_sensitivity |
float 0.1–10 | 1.0 | Replaces Pad::MouseSensitivity. |
swap_confirm |
bool | false | Swap Cross and Circle for controllers only. |
bindings |
table name → [keys] |
the current table | cross = ["Return", "Space"], left_stick_left = ["A"], r2 = ["MouseRight"], up = ["WheelUp"]. SDL scancode names are used. An unknown key name is a validation error. |
- Output calls. (Implemented on
mainafter beanportps5-j7ds; not verified on hardware.)SetVibrationforwards toSDL_GameControllerRumble(amplitudes scaled by 257, refreshed every 700 ms while held because SDL rumble lasts 2 s). Rumble is basic XInput/HIDAPI rumble, not DualSense haptics.SetLightBarforwards throughSDL_GameControllerSetLEDon controllers that report an LED;ResetLightBarandscePadCloserestore the default colour.SetTriggerEffectdecodes the guestScePadTriggerEffectParamand, on a PS5 controller, sends it as an SDL DS5 effects report; other pads with trigger motors get a rumble fallback. Post-1.0, best effort.scePadClosereturns the slot's request to defaults, so rumble, the light bar and trigger effects never stick on the host pad.- Attach logs the controller's capabilities once, so no call silently stubs without a trace.
- Guest keyboard and mouse.
libSceMouseandlibSceKeyboardexports callUnsupported(), which logs and aborts, unless the M1 inventory shows a gate title imports them. No throw crosses theAPS5_VABIboundary (threading.md). If one does, they read the sameInputHubevents.
| Peer spec | Contract |
|---|---|
| configuration.md | The [input] section, its typed validation, and the APS5_NO_PAD_INPUT replacement (debug.ignore_host_input during recorded-input replay). |
| save-data.md | The user IDs from libSceUserService that slots map to. |
| threading.md | scePadRead takes one short host mutex, with no global guest lock. Events are processed on the window thread. |
| gpu-driver.md | Presentation shares the window loop that pumps SDL events. The Presenter must not block event pumping for more than one frame. |
| build-toolchain.md | SDL CMake options (SDL_JOYSTICK and SDL_HIDAPI on from M2) and the SDL pin check. |
| verification.md | Recorded-input replay injects at InputHub, below the device layer, so replays are identical on any host controller. |
| Failure | Handling |
|---|---|
| No controller and keyboard/mouse only | Slot 0 is still connected, with keyboard/mouse bindings. |
| Controller unplugged mid-game | connected = false for that slot, and the title shows its own reconnect prompt. Replugging restores the slot. |
| Two controllers on one slot, via keyboard merge | Buttons are OR-merged. For the sticks, the value furthest from centre wins. |
| DualSense on an SDL build without HIDAPI | Falls back to generic DirectInput mapping, with a warning logged. This is a failure in the gate matrix. |
| Window loses focus | Every key is released and mouse-look is released (current behaviour, PadInput.cpp:24-29). |
| An invalid binding name in TOML | Start-up error naming the key and the file (see configuration.md). |
- GoogleTest Unit Suites (
ctest -L unit, hostedunitjob):- Button OR-merging and stick displacement arbitration rules (
PadHapticsTests.cpp). - Radial and axial dead-zone mathematics and clamp boundaries (
PadHapticsTests.cpp). - Controller button/axis/trigger translation, slot merge with keyboard, and per-slot connect/disconnect through
scePadRead(tests/input/ControllerInputTests.cpp, synthetic samples, no device). - Slot assignment policy across plug and unplug sequences: lowest free slot, same-GUID reclaim, duplicate and fifth-controller handling (
tests/input/PadSlotTableTests.cpp, pureSlotTable, synthetic ids and GUIDs). - The same sequences through real SDL device events into
PadInput(the glue is not covered; beanportps5-de24). - Output queue contracts: vibration, light bar, trigger effect, vibration mode, reset on close, per-slot isolation and SCE error codes (
tests/input/PadOutputExportTests.cpp). - Trigger effect encodings for all six modes, param parsing of null, short and malformed buffers, rumble scaling and the DS5 effects report offsets (
tests/input/PadOutputMappingTests.cpp). - Motion and touchpad maths: g scaling, Mahony fusion with NaN and step guards, touch scaling and ids (
tests/input/PadMotionTests.cpp). - SDL is built with the HIDAPI joystick driver and a PS5-capable pin (
tests/input/PadSdlBuildTests.cpp). - TOML controller binding parsing and rejection of invalid identifiers (bean
portps5-de24). - Monotonic timestamp advancement invariants on sequential
scePadReadcalls (PadHapticsTests.cpp). - Mouse open/read/close error contract + SDL routing + ring overflow (
libSceMouse/tests/Mouse.cpp, M2-gated DISABLED GTest; builds in CI, enables with the M2 exports; beanportps5-afme).
- Button OR-merging and stick displacement arbitration rules (
- Ported Ecosystem Test Suites:
- KytyPS5
PadHapticsTests: DualSense USB report parsing, radial deadzone calculation, motor vibration amplitude translation, and controller orientation telemetry (core/libs/tests/PadHapticsTests.cpp).
- KytyPS5
- Replay determinism: the same recorded input produces identical
PadDatasequences whatever controller backend is present. - Manual matrix per release: a small table recorded in the release notes (the device classes only, no personal hardware):
| Device class | Buttons | Sticks/triggers | Hot-plug | Rumble |
|---|---|---|---|---|
| XInput pad | ✓ | ✓ | ✓ | ✓ |
| DualSense USB | ✓ | ✓ | ✓ | basic |
| Keyboard + mouse | ✓ | digital/mouse | n/a | n/a |
- Full run: each gate title is played to credits on one controller class, and the menus are checked on the other two.
| Milestone | Work |
|---|---|
| M1 | - [x] Port AnyPS5 main's scePadRead (merged PR #5): landed with PR #15, extended with SDL controllers in PRs #54 and #57.- [x] Replace APS5_NO_PAD_INPUT with debug.ignore_host_input (PadInput.cpp:27-31).- [ ] Import inventory for Mouse and Keyboard: only a code comment records that no gate title imports libSceMouse at boot; per-title evidence is bean portps5-3eh1.Sync status (PR #28): the mouse backend ( libSceMouse/src/mouse_impl.cpp, include/MouseState.hpp, include/mouse_structs.h) plus VideoOut routing (libSceVideoOut/src/MouseInput.cpp, include/MouseInput.hpp) are byte-identical to upstream 53bda68; libSceMouse/Export.cpp still throws, so the Current-state row above stands. tests/Mouse.cpp is converted to GTest but DISABLED until the M2 exports land (builds in CI to pin the API). Debt: the process-global std::mutex mouseMutex (mouse_impl.cpp:9) violates the no-global-locks rule and must go with the M2 work. |
| M2 | - [x] DualSense output path in software (rumble, light bar, trigger effects, motion, touchpad, hot-plug slot table), bean portps5-j7ds.- [ ] Everything in the target design. XInput, DualSense USB and keyboard/mouse pass the matrix on Dreaming Sarah and TMNT (the F5 delivery milestone; bean portps5-de24). |
| M3–M5 | - [ ] Regression only. Add analog-trigger and multi-button coverage as the 3D titles demand. |
| M6 | - [ ] The release matrix is published in the release notes. |
- Do any gate titles need a second local player (TMNT supports co-op)? 1.0 gates single-player only. A second slot works in the design but is not gated.
- Gyro: motion now flows from SDL joystick sensors into
scePadRead(they do not need theSDL_SENSORsubsystem; noSDL_SENSOR_DISABLEDguard exists in the pinned joystick code). Does any gate title need motion, and are the fused orientation axis signs right on a physical DualSense (beanportps5-ds7h)? - Is the default light bar colour (0, 64, 255, inferred from AnyPS5
85517679) what a console shows afterscePadResetLightBar? libScePadhas no guest-memory range check (beanportps5-8l0d):scePadSetTriggerEffectnull-checks the guest pointer and then reads 120 bytes.- Should rumble on XInput stay on by default? It adds no DualSense-specific behaviour.
- Attach:
SDL_CONTROLLERDEVICEADDED->SDL_GameControllerOpen-> lowest free slot;REMOVEDfrees it. A fifth controller is ignored. - Buttons: A/B/X/Y = Cross/Circle/Square/Triangle, START = Options, shoulders = L1/R1, stick clicks = L3/R3, D-pad, touchpad click = TouchPad bit.
- Sticks: -32768..32767 -> 0..255 (centre 128) with a radial dead zone from
[input] deadzone(default 0.08), rescaled so output starts at 0 at the zone edge. - Triggers: 0..32767 -> 0..255 in
analog_buttons_l2/r2; the digital L2/R2 bit is raised above 30/255. - Slot 0 merge: buttons OR, sticks furthest from centre, keyboard R2/L2 still force 255.
debug.ignore_host_input: controllers are never attached while it is set. On window focus loss every attached controller publishes neutral state.- Slots 1..3 can be opened with
scePadOpenonly while a controller occupies them.
Build: SDL_JOYSTICK and SDL_HIDAPI are ON, SDL_HAPTIC and SDL_SENSOR stay OFF. DualSense IMU, touchpad, LED, rumble and effects all go through the HIDAPI PS5 driver of the pinned SDL (3rdparty/SDL2, 2.33 development tree, src/joystick/hidapi/SDL_hidapi_ps5.c). Over USB the driver always uses its enhanced reports, so no SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE hint is set (the hint only matters for Bluetooth, out of scope). SDL parses and CRC-checks the HID reports; PortPS5 never sees raw reports, so there is no host-side report parser to bounds-check. The DualSenseReportTest cases in core/libs/tests/PadHapticsTests.cpp use a test-local toy report struct and prove field pass-through only.
| Piece | File | Notes |
|---|---|---|
| Rumble and trigger encodings | libScePad/include/PadOutputMapping.hpp |
pure; DS5 effects payload offsets match DS5EffectsState_t in the pinned SDL |
| Motion, touch | libScePad/include/PadMotion.hpp |
pure; SDL units in (m/s^2, rad/s), guest g out |
| Slot policy | libScePad/include/PadSlotTable.hpp |
pure; a replacement pad lands in the lowest free slot |
| Output queue, fusion | libScePad/src/PadState.cpp |
per slot, one mutex |
| SDL side | libSceVideoOut/src/PadInput.cpp |
window thread only |
Behaviour:
- Motion defaults to on and is live only when the controller delivers sensor data and the guest has not disabled it with
scePadSetMotionSensorState(false). Otherwise the pad reports the rest pose (g on +Y, identity orientation). - Touchpad fingers (two) are reported with ids that change on every new contact. When no finger is down, slot 0 keeps the keyboard
TouchLeft/TouchRightemulation. - Invalid trigger effect payload values (out of range for a mode) are treated as "off". A mode above 6 or mask bits above 1 return
PAD_ERROR_INVALID_ARG. Only the selected trigger's command is decoded. - Licence: SDL is zlib. The bundled
src/hidapiis offered under GPL-3.0, BSD-style or the original HIDAPI licence at the user's choice; PortPS5 uses the BSD-style terms (3rdparty/SDL2/src/hidapi/LICENSE-bsd.txt). No Apache-2.0 code is linked.
Upstream attribution (AnyPS5, GPL-2.0): e2609a87 (HIDAPI; already present, hint skipped), 4c349efe (output and motion plumbing, adapted from one global pad state to per-slot state, no raw memcpy of mode, bounds-checked parsing), 1d245cbe (SDL sensor, LED, rumble and effect forwarding, adapted to the per-slot controller table), 85517679 (export bodies; scePadGetInfo with an invented Bluetooth address and scePadSetProcessPrivilege/SetParticularMode skipped). dee927ea (speaker and actuator audio) is not ported (bean portps5-ds7a).
Not verified (needs a physical USB DualSense, kept as "verified locally by the maintainer", bean portps5-ds7h): HIDAPI driver selection on Windows, rumble feel, light bar colour, the 11-byte trigger blocks, sensor axis signs and fusion convergence, touchpad coordinates, unplug and replug timing.