psx_mod_set_pad_transform(player, &PSXModPadTransform) registers stage 2b of pad_external_input: after the physical pad and any controller source, before the mouse policy. The transform sees the resolved pad plus the port's host extras (gamepad present, LT/RT 0..255 from SDL, unthresholded) and may rewrite buttons, sticks and the presented controller type, e.g. a NeGcon whose I/II come from the triggers. Output is pre-filled as a pass-through, validated (struct size, ranges, type within allowed_types) and replaced by neutral when invalid; detaching or a mod/session reset owes one neutral release frame. initial_type sets the boot/hotplug type. The resolver stays pure: host extras arrive through a new capture hook and the transform through another, both covered in pad_external_input_test. Under the input guard the transform runs on zero triggers and its output is neutralized. A NeGcon result resets the mouse policy rather than letting it overwrite the I/II bytes. Offline only: netplay, resim, selfcheck replay and plain debug overrides never reach it. With no transform registered every pad is bit-identical to before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.5 KiB
Optional local mouse policy
The SDL-free PSXModMousePolicy interface provides one local P1 policy.
Register it, like a controller source, from mod code on the main thread (see
mod_plugins.h: one registration rule, mod-trusted, no caller check). Registration copies the policy,
rejects incompatible struct sizes, missing callbacks and a second policy,
and returns success/failure. Existing const-input controller presentation is
unchanged. The generic runtime has no game addresses or gesture recognizer.
eligible(native_buttons) supplies the game's current persistent-state
predicate. event() receives ordered reset/acquired/motion/hold edges with
nanosecond event times and fractional displacement. sample(now, output) is
a non-consuming query and may override only the right axes. The output is
initialized to neutral; its size, override flag and byte bounds are checked.
SDL event delivery and relative capture remain on the existing SDL owner thread. SDL3 timestamps share SDL_GetTicksNS's epoch. SDL2 millisecond event timestamps are lifted into the nearest current 64-bit era; older SDL2 versions use a private owner-thread tick extension. No SDL public types enter the game interface. No secondary event queue or generalized arbitration is added.
Only a fresh game-window LEFT mouse-button down can acquire capture, with focused, visible, eligible, connected analog P1 local gameplay. The acquisition source and captured Escape are suppressed through release before keyboard/mouse binding folding. LEFT release immediately releases capture and resets the policy, including a separately held direction; it is never timestamp-gated. Both normal edges and emergency Escape are silent. Only genuine capture/config failures produce bounded diagnostics. Suppression is per source/player, so a controller holding the same PSX action remains intact. Primary and alternate P1 bindings are inspected for hold-control conflicts without rewriting them. Ordinary bindings remain live; a conflict disables only hold interpretation with a notice. Physical down-state and accepted hold-state are separate. Only an accepted hold receives HOLD_RELEASE; conflicting or acquisition-blocked releases leave a free flick intact. A polled host-hotkey keyboard view also excludes consumed P1 sources, so captured Escape cannot activate a configured Turbo=Escape. The original SDL state, controller shortcuts and P2 folds remain available.
Local P1 RX/RY are applied after all native folding and presentation, before the existing PsxNetPad/SIO publication. Whole-vector native right-stick activity releases mouse capture and retains the native bytes. Other seats, buttons, left axes and report type/order are not rewritten. With no policy, the existing post-pacer SDL_PumpEvents timing is retained.
Focus loss, hide/minimize, host or game menus, reset, state load, rewind, routing/removal, invalid data, capture failure and >250 ms host stalls release capture and reset the policy. Stale, backwards and future motion cannot extend a pulse. Releases remain immediate even with anomalous timestamps; their watermark cannot admit old motion after a cleared held target. A valid ordered LEFT release while capture remains eligible uses its actual event timestamp as the acquisition boundary. A later physical press in the same delayed SDL drain can therefore acquire a neutral new session. Lifecycle, stall and malformed-release resets retain the processing-time barrier; a release after such a reset cannot lower it to admit queued input. A stall's backlog cannot acquire capture again. Eligibility recovery needs a release/repress of LEFT and fresh motion. Reset retains physical LEFT down-state, so holding it across an interruption cannot recapture. Debug input injection, headless, netplay, replay/resimulation and render-only passes inhibit this policy.
Order with offline controller sources
Per player the runtime resolves, in pad_external_input.c: (1) physical
capture plus the port's host extras (gamepad present, LT/RT 0..255),
(2) an offline controller source (psx_mod_set_controller_source:
buttons = source AND physical, sticks/type from the source), (2b) a title pad
transform (psx_mod_set_pad_transform: sees that pad and the host extras,
may rewrite buttons, sticks and the presented controller type; output
validated against its allowed types, neutral when invalid, one neutral
release frame on detach/reset), (3) this mouse policy, P1 only, which may
override only the right axes of that result. A NeGcon result has pressure
bytes where the right stick was, so it resets the mouse instead. With no
transform registered stage 2b is a no-op and the pad is bit-identical. A
source never suppresses the mouse policy: it sees the final buttons, analog
flag and right stick, so capture is never taken and then discarded, and a
deflected source right stick, a digital pad or Start held disable it just as a
physical pad would. A source release/decline frame, no device, or an armed
input guard resets the capture. pad_ext_live is the single mouse gate list
(debug override, headless, netplay/resim, selfcheck lock/resim, render pass,
savestate menu, rewind, input guard). With no policy registered
psx_local_mouse_begin/pad return immediately: no per-frame mouse or focus
polling.
psx_local_mouse_clear() unregisters the policy during existing mod/session
reset. Source suppression already owed through release survives unregister,
so disabling a mod cannot leak a held Escape into a native binding. There is
one P1 policy; physical device locking and P2 mouse controls are out of scope.
Validation status and targets
The preceding click-toggle candidate passed a full SDL3 Release link, focused
tests, both SDL backend input checks and a private cold-start smoke check.
The LEFT-held activation revision passes the focused policy/reducer integration,
both SDL backend input checks, native SIO checks and a full incremental SDL3
Release link with five registered game tests. Its new private candidate passes
static import/profile checks; physical startup/interruption validation remains
pending. local_mouse_policy_test supplies a deterministic fake backend and
covers capture, neutral acquisition, releases, host/guest gates, bad times,
stalls, conflicting hold, failure fallback and registration validation.
mouse_binding_suppression_test folds the real keybind code with a controlled
mouse poll, covering primary/alternate/button/axis consumption through release
and independent controller/P2 actions. keyboard_pad_chord_test includes
captured-Escape suppression and fresh native re-presses.
It also checks repeated Turbo=Escape polls and the independent P2 report.
Run these and the existing controller presentation, stick mapping, netplay pad codec and SIO tests with both supported SDL backends. Compile the actual adapter/main path with debug tools enabled and disabled. Observe relative capture/focus/menu/removal and normal report delivery in a private game copy; pure policy tests do not exercise the OS or game dispatcher.
The reviewed implementation base is release framework commit
065888f50f, also the canonical
release/ape-v0.5.0-hle-support branch. Current upstream master has differing
existing mod APIs and needs separate compatibility validation. The linked game
draft pins this runtime draft's reachable head through its configured framework
upstream; final-pin checkout, build and local test validation are recorded with
the drafts. Hosted CI status and physical interruption checks are reported
separately; this is not a
public-quality or merge-readiness claim.