mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge pull request #510 from RetroPortingToolKit/feat/unified-external-input
Unified external input: controller sources and P1 mouse policy in one ordered layer
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# 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, (2) an offline controller source (`psx_mod_set_controller_source`:
|
||||
buttons = source AND physical, sticks/type from the source), (3) this mouse
|
||||
policy, P1 only, which may override only the right axes of that result. 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
|
||||
065888f50f9131839bcbbc8814debf58b4624b16, 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.
|
||||
+17
-1
@@ -27,6 +27,10 @@ if(BUILD_TESTING)
|
||||
../recompiler/src/mips_decoder.cpp)
|
||||
target_include_directories(disasm_test PRIVATE include ../recompiler/include)
|
||||
add_test(NAME disasm_test COMMAND disasm_test)
|
||||
add_executable(local_mouse_policy_test
|
||||
tests/test_local_mouse_policy.cpp src/local_mouse_policy.cpp)
|
||||
target_include_directories(local_mouse_policy_test PRIVATE include src)
|
||||
add_test(NAME local_mouse_policy_test COMMAND local_mouse_policy_test)
|
||||
# Netplay: which BIOS a match boots, and its wire helpers.
|
||||
add_executable(netplay_bios_settle_test
|
||||
tests/test_netplay_bios_settle.c
|
||||
@@ -364,7 +368,7 @@ if(BUILD_TESTING)
|
||||
|
||||
add_executable(keyboard_pad_chord_test
|
||||
tests/test_keyboard_pad_chord.c
|
||||
src/psx_keybinds.c)
|
||||
src/psx_keybinds.c src/host_keymap.c)
|
||||
target_include_directories(keyboard_pad_chord_test PRIVATE
|
||||
include ${PSX_SDL_INCLUDE_DIRS})
|
||||
target_compile_definitions(keyboard_pad_chord_test PRIVATE
|
||||
@@ -373,6 +377,13 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(keyboard_pad_chord_test PRIVATE ${PSX_SDL_LIBRARIES})
|
||||
add_test(NAME keyboard_pad_chord_test COMMAND keyboard_pad_chord_test)
|
||||
|
||||
add_executable(mouse_binding_suppression_test tests/test_mouse_binding_suppression.c)
|
||||
target_include_directories(mouse_binding_suppression_test PRIVATE include ${PSX_SDL_INCLUDE_DIRS})
|
||||
target_compile_definitions(mouse_binding_suppression_test PRIVATE
|
||||
SDL_MAIN_HANDLED $<$<BOOL:${PSX_SDL3}>:PSX_SDL3=1>)
|
||||
target_link_libraries(mouse_binding_suppression_test PRIVATE ${PSX_SDL_LIBRARIES})
|
||||
add_test(NAME mouse_binding_suppression_test COMMAND mouse_binding_suppression_test)
|
||||
|
||||
# Generic external replacement-pack core. The renderer integration is a
|
||||
# separate consumer; this target pins pack discovery, deterministic keying,
|
||||
# VRAM residency, portable tracking state, and bounded asynchronous decode.
|
||||
@@ -425,6 +436,11 @@ if(BUILD_TESTING)
|
||||
target_include_directories(mod_controller_source_test PRIVATE include)
|
||||
add_test(NAME mod_controller_source_test COMMAND mod_controller_source_test)
|
||||
|
||||
add_executable(pad_external_input_test
|
||||
tests/test_pad_external_input.c src/pad_external_input.c)
|
||||
target_include_directories(pad_external_input_test PRIVATE include src)
|
||||
add_test(NAME pad_external_input_test COMMAND pad_external_input_test)
|
||||
|
||||
add_executable(gte_register_access_test
|
||||
tests/test_gte_register_access.cpp
|
||||
src/gte.cpp
|
||||
|
||||
@@ -756,25 +756,86 @@ int psx_mod_set_controller_presentation_policy(
|
||||
uint32_t initial_mode,
|
||||
int config_capable);
|
||||
|
||||
/* Offline controller source owns a player's pad at normal input sampling.
|
||||
* Mod-trusted like any mod code (arbitrary native code; the setter does no
|
||||
* caller check). A declined/invalid sample delivers neutral, not the previous
|
||||
* held input.
|
||||
* Existing TCP overrides take priority; netplay/resim and eye redraws never
|
||||
* invoke the source. The runtime keeps coherent SIO type requests/recording.
|
||||
* Pass NULL to detach. Local keyboard/pad buttons remain merged for menus;
|
||||
* the source owns sticks and type. No source leaves faithful defaults intact.
|
||||
* Source and setter are main (emulation) thread only. Sources are sampled once
|
||||
* per frame, only by the normal offline sampler: not in headless mode, not in
|
||||
* netplay. The source's `analog` is the pad's capability; the final type still
|
||||
* goes through the multitap rule, mod mode override and presentation policy.
|
||||
* A bad struct_size or out-of-range value delivers neutral and logs to stderr. */
|
||||
/* ---- External offline input: ONE ordered resolution per player ----------
|
||||
* Two optional, mod-supplied inputs can act on an offline player's pad. The
|
||||
* runtime resolves them in a fixed order in pad_external_input.h:
|
||||
* 1. physical/local capture (keyboard, controllers: buttons, sticks, type)
|
||||
* 2. offline controller source (psx_mod_set_controller_source): buttons =
|
||||
* source AND physical; sticks/type come from the source, through the
|
||||
* same mode override / multitap rule / presentation policy as a physical
|
||||
* pad. A declined/invalid sample delivers neutral, not the last input.
|
||||
* 3. local mouse policy (psx_mod_set_local_mouse_policy), P1 only: may
|
||||
* override ONLY the right analog axes of the pad resolved by 1-2. It sees
|
||||
* the final buttons/analog flag/right stick, so it composes with a
|
||||
* source (a source never suppresses it) and no capture is taken to be
|
||||
* discarded. A deflected source right stick, a digital pad, or Start
|
||||
* held disables it exactly as for a physical pad; a source release,
|
||||
* no device, or an armed input guard resets (releases) the capture.
|
||||
* Gating. Neither input reaches the guest under netplay or rollback resim,
|
||||
* selfcheck replay, headless, or a debug-server input override (which wins and
|
||||
* is never mixed with external input). The mouse policy is additionally live
|
||||
* only outside selfcheck input lock/resim, render passes, rewind, the
|
||||
* savestate menu and the savestate input guard (pad_ext_live). A source is
|
||||
* still sampled while the input guard is armed but its output is discarded
|
||||
* (neutral). With no source and no mouse policy registered, pad bytes and
|
||||
* host event handling are exactly the faithful defaults.
|
||||
*
|
||||
* ONE registration rule for both: call only on the main (emulation) thread;
|
||||
* struct arguments are validated at registration (struct_size) and a bad one
|
||||
* is rejected (return 0, logged to stderr); results of each callback are
|
||||
* validated per use and fall back to neutral. These are mod-trusted APIs --
|
||||
* arbitrary native code, no caller check -- not launcher settings. A source
|
||||
* is per player (NULL detaches, with one neutral release frame); the mouse
|
||||
* policy is one per session (a second registration returns 0). Callbacks run
|
||||
* on the main thread only: sources once per frame, in the normal offline
|
||||
* sampler.
|
||||
* The source's `analog` is the pad capability; the final type still goes
|
||||
* through the multitap rule, mod mode override and presentation policy.
|
||||
*/
|
||||
typedef struct PSXModControllerState {
|
||||
uint32_t struct_size, buttons, lx, ly, rx, ry, analog;
|
||||
} PSXModControllerState;
|
||||
typedef int (*PSXModControllerSource)(PSXModControllerState *state);
|
||||
int psx_mod_set_controller_source(uint32_t player, PSXModControllerSource source);
|
||||
|
||||
/* Local P1 mouse policy. The runtime delivers ordered events on the SDL owner
|
||||
* (main) thread, owns relative capture and folds the resulting right-stick
|
||||
* bytes after native input/presentation (and after any controller source),
|
||||
* before normal SIO delivery. No SDL type, guest address, gesture, or game
|
||||
* setting belongs in this interface. */
|
||||
enum {
|
||||
PSX_MOD_MOUSE_HOLD_NONE = 0,
|
||||
PSX_MOD_MOUSE_HOLD_RIGHT = 1, /* Mouse3 in keybinds.ini */
|
||||
PSX_MOD_MOUSE_HOLD_LEFT_ALT = 2,
|
||||
PSX_MOD_MOUSE_RESET = 0,
|
||||
PSX_MOD_MOUSE_ACQUIRED = 1,
|
||||
PSX_MOD_MOUSE_MOTION = 2,
|
||||
PSX_MOD_MOUSE_HOLD_PRESS = 3,
|
||||
PSX_MOD_MOUSE_HOLD_RELEASE = 4
|
||||
};
|
||||
typedef struct PSXModMouseEvent {
|
||||
uint32_t struct_size;
|
||||
uint32_t type;
|
||||
uint64_t time_ns;
|
||||
double dx, dy;
|
||||
} PSXModMouseEvent;
|
||||
typedef struct PSXModMouseOutput {
|
||||
uint32_t struct_size;
|
||||
uint32_t override_right;
|
||||
uint32_t rx, ry;
|
||||
} PSXModMouseOutput;
|
||||
typedef struct PSXModMousePolicy {
|
||||
uint32_t struct_size;
|
||||
uint32_t hold_control;
|
||||
/* Re-read persistent guest context, including pause/menus, on every event
|
||||
* and local sample. Must not infer it from a host Start toggle. */
|
||||
int (*eligible)(uint32_t native_buttons);
|
||||
void (*event)(const PSXModMouseEvent* event);
|
||||
/* A query: must not consume motion, change the event anchor or deadlines. */
|
||||
void (*sample)(uint64_t now_ns, PSXModMouseOutput* output);
|
||||
} PSXModMousePolicy;
|
||||
int psx_mod_set_local_mouse_policy(const PSXModMousePolicy* policy);
|
||||
|
||||
/*
|
||||
* Register a C plugin before main() on the compilers supported by the runtime.
|
||||
* The registry itself uses function-local initialization, so constructor order
|
||||
|
||||
@@ -31,6 +31,13 @@ extern "C" {
|
||||
|
||||
#define PSXKB_MAX_PLAYERS 5
|
||||
|
||||
/* Shared pseudo-scancodes for INI Mouse1..Mouse5. Keep them outside SDL's
|
||||
* keyboard array and check them before indexing it. Mouse3 is right. */
|
||||
#define PSXKB_MOUSE_SC_BASE 512
|
||||
#define PSXKB_MOUSE_SC(btn) ((SDL_Scancode)(PSXKB_MOUSE_SC_BASE + (btn)))
|
||||
#define PSXKB_IS_MOUSE_SC(sc) \
|
||||
((int)(sc) > PSXKB_MOUSE_SC_BASE && (int)(sc) <= PSXKB_MOUSE_SC_BASE + 5)
|
||||
|
||||
/* Button indices — stable order, matches the order keybinds.ini writes and the
|
||||
* order the launcher's rebind chips are laid out. Keep in sync with kButtons[]
|
||||
* in psx_keybinds.c. */
|
||||
@@ -67,6 +74,16 @@ void psx_keybinds_init(const char *exe_path);
|
||||
/* Read-only view of the current bindings. */
|
||||
const PsxKeyBinds *psx_keybinds_get(void);
|
||||
|
||||
/* Runtime-owned host controls consumed by optional local input capture.
|
||||
* Suppression is per source/player, before PSX folding, and lasts through
|
||||
* release. It never clears a merged PSX button held by another source. */
|
||||
void psx_keybinds_suppress_control(int player, SDL_Scancode sc);
|
||||
void psx_keybinds_release_control(int player, SDL_Scancode sc);
|
||||
int psx_keybinds_control_suppressed(int player, SDL_Scancode sc);
|
||||
/* Host hotkey polling gets a keyboard view excluding consumed P1 sources.
|
||||
* The original SDL state and other players' PSX folds remain unchanged. */
|
||||
void psx_keybinds_host_keys(const uint8_t *keys, uint8_t out[SDL_NUM_SCANCODES]);
|
||||
|
||||
/* ── Runtime read helpers (keyboard -> PSX pad), player is 1..PSXKB_MAX_PLAYERS */
|
||||
|
||||
/* Build the 16-bit ACTIVE-LOW PSX button word for `player` from the SDL
|
||||
|
||||
@@ -341,6 +341,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_window_icon.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_sdl_audio.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_stick.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/local_mouse_policy.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/local_mouse_sdl.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/memory.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_ram_geometry.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/kernel_patch_ranges.c
|
||||
@@ -354,6 +356,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_openxr.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/pad_external_input.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/vr_pose_math.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dma_gpu_ll.c
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "local_mouse_policy.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace psx {
|
||||
namespace { constexpr uint64_t StallNs = 250000000; }
|
||||
|
||||
bool LocalMousePolicy::install(const PSXModMousePolicy* policy) {
|
||||
if (installed() || !policy || policy->struct_size != sizeof(*policy) ||
|
||||
policy->hold_control > PSX_MOD_MOUSE_HOLD_LEFT_ALT ||
|
||||
!policy->eligible || !policy->event || !policy->sample) return false;
|
||||
policy_ = *policy;
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
void LocalMousePolicy::clear() {
|
||||
reset();
|
||||
policy_ = {};
|
||||
host_ = {};
|
||||
hold_down_ = hold_blocked_ = hold_active_ = false;
|
||||
activation_down_ = activation_owned_ = false;
|
||||
event_valid_ = false;
|
||||
clock_valid_ = false;
|
||||
clock_ = event_clock_ = release_clock_ = 0;
|
||||
conflict_noticed_ = capture_failure_noticed_ = false;
|
||||
}
|
||||
void LocalMousePolicy::dispatch(uint32_t type, uint64_t t, double dx, double dy) {
|
||||
if (!installed()) return;
|
||||
const PSXModMouseEvent event{sizeof(PSXModMouseEvent), type, t, dx, dy};
|
||||
policy_.event(&event);
|
||||
}
|
||||
void LocalMousePolicy::reset() {
|
||||
if (captured_) backend_.capture(backend_.context, false);
|
||||
captured_ = false;
|
||||
release_clock_ = clock_;
|
||||
event_valid_ = false;
|
||||
hold_blocked_ = hold_down_;
|
||||
hold_active_ = false;
|
||||
dispatch(PSX_MOD_MOUSE_RESET); // releases remain immediate even with bad time
|
||||
}
|
||||
MouseControl LocalMousePolicy::hold_control() const {
|
||||
if (policy_.hold_control == PSX_MOD_MOUSE_HOLD_RIGHT) return MouseControl::Right;
|
||||
if (policy_.hold_control == PSX_MOD_MOUSE_HOLD_LEFT_ALT) return MouseControl::LeftAlt;
|
||||
return MouseControl::None;
|
||||
}
|
||||
bool LocalMousePolicy::allowed() const {
|
||||
return installed() && host_.live && host_.focused && host_.connected &&
|
||||
host_.analog && !host_.native_right && !host_.start && policy_.eligible(host_.buttons);
|
||||
}
|
||||
void LocalMousePolicy::update(uint64_t now, LocalMouseHost host) {
|
||||
const bool stalled = clock_valid_ && (now < clock_ || now - clock_ > StallNs);
|
||||
clock_ = now;
|
||||
clock_valid_ = true;
|
||||
host_ = host;
|
||||
if (stalled || !allowed()) reset();
|
||||
if (host.hold_conflict && !conflict_noticed_) {
|
||||
backend_.notice(backend_.context, "Mouse hold disabled: control is bound to P1");
|
||||
conflict_noticed_ = true;
|
||||
}
|
||||
if (!host.hold_conflict) conflict_noticed_ = false;
|
||||
if (captured_ && host.hold_conflict && !hold_blocked_) {
|
||||
hold_blocked_ = true;
|
||||
if (hold_active_) {
|
||||
hold_active_ = false;
|
||||
dispatch(PSX_MOD_MOUSE_HOLD_RELEASE);
|
||||
event_clock_ = std::max(event_clock_, now);
|
||||
event_valid_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool LocalMousePolicy::valid_time(uint64_t t, uint64_t now) {
|
||||
if (t > now || t < release_clock_ || now - t > StallNs ||
|
||||
(event_valid_ && t < event_clock_)) {
|
||||
reset();
|
||||
return false;
|
||||
}
|
||||
event_clock_ = t;
|
||||
event_valid_ = true;
|
||||
return true;
|
||||
}
|
||||
bool LocalMousePolicy::control(uint64_t t, uint64_t now, MouseControl control,
|
||||
bool down, bool repeat) {
|
||||
if (control == MouseControl::Left) {
|
||||
const bool fresh = down && !activation_down_ && !repeat;
|
||||
activation_down_ = down;
|
||||
if (!down) {
|
||||
// Release is unconditional: a future/backwards event time cannot
|
||||
// leave capture or the reducer's separate direction hold active.
|
||||
// A valid ordered release while still eligible owns its event
|
||||
// boundary, rather than the later time at which SDL drains it.
|
||||
// After an interruption or malformed release, retain reset()'s
|
||||
// processing-time barrier so queued input cannot rearm capture.
|
||||
const bool ordered_release = captured_ && allowed() &&
|
||||
t <= now && now - t <= StallNs && t >= release_clock_ &&
|
||||
(!event_valid_ || t >= event_clock_) &&
|
||||
(!clock_valid_ || (now >= clock_ && now - clock_ <= StallNs));
|
||||
const bool owned = activation_owned_;
|
||||
activation_owned_ = false;
|
||||
if (captured_ || owned) reset();
|
||||
if (ordered_release) release_clock_ = t;
|
||||
return owned;
|
||||
}
|
||||
// reset() retains physical down-state. A held button after a menu,
|
||||
// focus loss, native takeover or bad input is never a new activation.
|
||||
if (!fresh || captured_ || !allowed() || !valid_time(t, now)) return false;
|
||||
backend_.suppress(backend_.context, control);
|
||||
activation_owned_ = true;
|
||||
if (!backend_.capture(backend_.context, true)) {
|
||||
(void)backend_.capture(backend_.context, false);
|
||||
reset();
|
||||
if (!capture_failure_noticed_) {
|
||||
backend_.notice(backend_.context, "Mouse capture failed; native controls remain active");
|
||||
capture_failure_noticed_ = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
captured_ = true;
|
||||
hold_blocked_ = hold_down_;
|
||||
dispatch(PSX_MOD_MOUSE_ACQUIRED, t);
|
||||
return true;
|
||||
}
|
||||
// Hold-release is never timestamp-gated. Track while released too so a
|
||||
// held control across acquisition/focus loss cannot become a new press.
|
||||
if (control != MouseControl::None && control == hold_control()) {
|
||||
const bool fresh = down && !hold_down_ && !repeat;
|
||||
hold_down_ = down;
|
||||
if (!down) {
|
||||
hold_blocked_ = false;
|
||||
if (captured_ && hold_active_) {
|
||||
hold_active_ = false;
|
||||
dispatch(PSX_MOD_MOUSE_HOLD_RELEASE);
|
||||
// Release is immediate, but cannot move the event watermark
|
||||
// backwards and let older motion refill the cleared target.
|
||||
const uint64_t release_time = t <= now && now - t <= StallNs ? t : clock_;
|
||||
event_clock_ = std::max(event_clock_, release_time);
|
||||
event_valid_ = true;
|
||||
}
|
||||
} else if (captured_ && fresh && !host_.hold_conflict && !hold_blocked_) {
|
||||
if (valid_time(t, now)) {
|
||||
hold_active_ = true;
|
||||
dispatch(PSX_MOD_MOUSE_HOLD_PRESS, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (down && control == MouseControl::Escape && captured_) {
|
||||
backend_.suppress(backend_.context, control);
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void LocalMousePolicy::sync_left(uint64_t now, bool physically_down) {
|
||||
if (!physically_down && activation_down_)
|
||||
control(0, now, MouseControl::Left, false, false);
|
||||
}
|
||||
void LocalMousePolicy::motion(uint64_t t, uint64_t now, double dx, double dy) {
|
||||
if (!captured_) return;
|
||||
if (!allowed() || !std::isfinite(dx) || !std::isfinite(dy) ||
|
||||
std::abs(dx) > 1e9 || std::abs(dy) > 1e9) { reset(); return; }
|
||||
if (!valid_time(t, now) || (dx == 0.0 && dy == 0.0)) return;
|
||||
dispatch(PSX_MOD_MOUSE_MOTION, t, dx, dy);
|
||||
}
|
||||
void LocalMousePolicy::sample(uint64_t now, uint8_t& rx, uint8_t& ry) {
|
||||
if (!captured_) return;
|
||||
if (!allowed()) { reset(); return; }
|
||||
PSXModMouseOutput out{sizeof(PSXModMouseOutput), 0, 128, 128};
|
||||
policy_.sample(now, &out);
|
||||
if (out.struct_size != sizeof(out) || out.override_right > 1 || out.rx > 255 || out.ry > 255) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
if (out.override_right) { rx = uint8_t(out.rx); ry = uint8_t(out.ry); }
|
||||
}
|
||||
} // namespace psx
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
#include "mod_plugins.h"
|
||||
|
||||
namespace psx {
|
||||
|
||||
enum class MouseControl { None, Left = 1, Middle, Right, X1, X2, Escape, LeftAlt };
|
||||
struct LocalMouseHost {
|
||||
uint32_t buttons = 0xFFFF;
|
||||
bool live = false, focused = false, connected = false, analog = false;
|
||||
bool native_right = false, start = false, hold_conflict = false;
|
||||
};
|
||||
// Private backend seam also used by deterministic tests. No second event queue:
|
||||
// the existing owner-thread event drain dispatches synchronously in order.
|
||||
struct LocalMouseBackend {
|
||||
void* context;
|
||||
bool (*capture)(void*, bool);
|
||||
void (*suppress)(void*, MouseControl);
|
||||
void (*notice)(void*, const char*);
|
||||
};
|
||||
class LocalMousePolicy {
|
||||
public:
|
||||
explicit LocalMousePolicy(LocalMouseBackend backend) : backend_(backend) {}
|
||||
bool install(const PSXModMousePolicy* policy);
|
||||
void clear();
|
||||
void reset();
|
||||
void update(uint64_t now, LocalMouseHost host);
|
||||
bool control(uint64_t t, uint64_t now, MouseControl control, bool down, bool repeat);
|
||||
// Reconcile a LEFT release the host never delivered (e.g. made while unfocused).
|
||||
void sync_left(uint64_t now, bool physically_down);
|
||||
void motion(uint64_t t, uint64_t now, double dx, double dy);
|
||||
void sample(uint64_t now, uint8_t& rx, uint8_t& ry);
|
||||
bool captured() const { return captured_; }
|
||||
bool installed() const { return policy_.event != nullptr; }
|
||||
MouseControl hold_control() const;
|
||||
private:
|
||||
bool allowed() const;
|
||||
bool valid_time(uint64_t t, uint64_t now);
|
||||
void dispatch(uint32_t type, uint64_t t = 0, double dx = 0, double dy = 0);
|
||||
LocalMouseBackend backend_;
|
||||
PSXModMousePolicy policy_{};
|
||||
LocalMouseHost host_;
|
||||
uint64_t clock_ = 0, event_clock_ = 0, release_clock_ = 0;
|
||||
bool clock_valid_ = false, event_valid_ = false;
|
||||
bool captured_ = false, hold_down_ = false, hold_blocked_ = false;
|
||||
bool activation_down_ = false, activation_owned_ = false;
|
||||
bool hold_active_ = false; // accepted interpretation, separate from physical state
|
||||
bool conflict_noticed_ = false, capture_failure_noticed_ = false;
|
||||
};
|
||||
} // namespace psx
|
||||
@@ -0,0 +1,211 @@
|
||||
#include "local_mouse_sdl.h"
|
||||
#include "local_mouse_policy.h"
|
||||
#include "psx_keybinds.h"
|
||||
#include "host_osd.h"
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
SDL_Window* window_ = nullptr;
|
||||
psx::LocalMouseHost host_;
|
||||
|
||||
uint64_t now_ns() {
|
||||
#if defined(PSX_SDL3)
|
||||
return SDL_GetTicksNS();
|
||||
#else
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
return SDL_GetTicks64() * 1000000;
|
||||
#else
|
||||
// Preserve the framework's older SDL2 build path without requiring a
|
||||
// new dependency. This owner-thread clock extends the 32-bit tick era.
|
||||
static uint32_t previous = 0;
|
||||
static uint64_t era = 0;
|
||||
const uint32_t ticks = SDL_GetTicks();
|
||||
if (ticks < previous) era += uint64_t(1) << 32;
|
||||
previous = ticks;
|
||||
return (era + ticks) * 1000000;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
uint64_t event_ns(const SDL_Event& event, uint64_t now) {
|
||||
#if defined(PSX_SDL3)
|
||||
return event.common.timestamp;
|
||||
#else
|
||||
// Lift wrapping SDL2 millisecond timestamps into the nearest current era.
|
||||
const uint64_t ms = now / 1000000;
|
||||
const int32_t delta = static_cast<int32_t>(event.common.timestamp - uint32_t(ms));
|
||||
if (delta < 0 && uint64_t(-int64_t(delta)) > ms) return UINT64_MAX;
|
||||
return uint64_t(int64_t(ms) + delta) * 1000000;
|
||||
#endif
|
||||
}
|
||||
SDL_Scancode scancode(psx::MouseControl control) {
|
||||
if (control == psx::MouseControl::Escape) return SDL_SCANCODE_ESCAPE;
|
||||
if (control == psx::MouseControl::LeftAlt) return SDL_SCANCODE_LALT;
|
||||
if (control >= psx::MouseControl::Left && control <= psx::MouseControl::X2)
|
||||
return PSXKB_MOUSE_SC(int(control));
|
||||
return SDL_SCANCODE_UNKNOWN;
|
||||
}
|
||||
bool capture(void*, bool on) {
|
||||
if (!window_) return !on;
|
||||
#if defined(PSX_SDL3)
|
||||
return SDL_SetWindowRelativeMouseMode(window_, on);
|
||||
#else
|
||||
return SDL_SetRelativeMouseMode(on ? SDL_TRUE : SDL_FALSE) == 0;
|
||||
#endif
|
||||
}
|
||||
void suppress(void*, psx::MouseControl control) {
|
||||
psx_keybinds_suppress_control(1, scancode(control));
|
||||
}
|
||||
void notice(void*, const char* message) { host_osd_push(message, 2000); }
|
||||
psx::LocalMousePolicy& state() {
|
||||
static psx::LocalMousePolicy policy({nullptr, capture, suppress, notice});
|
||||
return policy;
|
||||
}
|
||||
bool focus() {
|
||||
if (!window_ || SDL_GetKeyboardFocus() != window_) return false;
|
||||
const auto flags = SDL_GetWindowFlags(window_);
|
||||
return (flags & (SDL_WINDOW_HIDDEN | SDL_WINDOW_MINIMIZED)) == 0;
|
||||
}
|
||||
bool conflict() {
|
||||
const SDL_Scancode sc = scancode(state().hold_control());
|
||||
if (sc == SDL_SCANCODE_UNKNOWN) return false;
|
||||
for (int i = 0; i < PSX_KB_COUNT; ++i)
|
||||
if (psx_keybinds_get_button(1, i) == sc || psx_keybinds_get_button_alt(1, i) == sc)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
void update(uint64_t now) {
|
||||
host_.focused = focus();
|
||||
host_.hold_conflict = conflict();
|
||||
state().update(now, host_);
|
||||
}
|
||||
bool control(uint64_t t, uint64_t now, psx::MouseControl control, bool down, bool repeat) {
|
||||
const SDL_Scancode sc = scancode(control);
|
||||
const bool swallowed = psx_keybinds_control_suppressed(1, sc) != 0;
|
||||
if (!down) psx_keybinds_release_control(1, sc);
|
||||
return state().control(t, now, control, down, repeat) || swallowed;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" int psx_mod_set_local_mouse_policy(const PSXModMousePolicy* policy) {
|
||||
if (state().install(policy)) return 1;
|
||||
std::fprintf(stderr, "psxrecomp: local mouse policy rejected (null/invalid, "
|
||||
"struct_size %u expected %u, or one is already registered)\n",
|
||||
policy ? (unsigned)policy->struct_size : 0u,
|
||||
(unsigned)sizeof(PSXModMousePolicy));
|
||||
return 0;
|
||||
}
|
||||
void psx_local_mouse_clear() { state().clear(); host_ = {}; window_ = nullptr; }
|
||||
bool psx_local_mouse_installed() { return state().installed(); }
|
||||
void psx_local_mouse_reset() {
|
||||
state().reset();
|
||||
host_.connected = host_.analog = false;
|
||||
}
|
||||
void psx_local_mouse_begin(SDL_Window* window, bool live) {
|
||||
// No policy: no host state to track, no per-frame global mouse polling.
|
||||
if (!state().installed()) return;
|
||||
if (window_ && window_ != window) state().reset();
|
||||
window_ = window;
|
||||
host_.live = live;
|
||||
update(now_ns());
|
||||
// Windows global state survives focus clearing SDL's cached window state.
|
||||
// Other backends may not support global mouse polling (e.g. Wayland), so
|
||||
// there reconcile whenever focused (captured or not), so a release made
|
||||
// while unfocused is caught on focus regain. Neither path manufactures a
|
||||
// down or unblocks a held activation across focus loss.
|
||||
#if defined(_WIN32)
|
||||
const bool left_down = (SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
|
||||
state().sync_left(now_ns(), left_down);
|
||||
#else
|
||||
if (host_.focused)
|
||||
state().sync_left(now_ns(), (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0);
|
||||
#endif
|
||||
// Catch a lost hold release, including one made outside the game window.
|
||||
const auto hold = state().hold_control();
|
||||
bool down = false;
|
||||
if (hold == psx::MouseControl::Right)
|
||||
down = (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
|
||||
else if (hold == psx::MouseControl::LeftAlt)
|
||||
down = SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LALT] != 0;
|
||||
if (hold != psx::MouseControl::None && !down)
|
||||
state().control(0, now_ns(), hold, false, false);
|
||||
}
|
||||
bool psx_local_mouse_event(const SDL_Event& ev) {
|
||||
if (!state().installed()) {
|
||||
// Ownership of an already consumed source lasts through release even
|
||||
// when its plugin has just been disabled/unregistered.
|
||||
SDL_Scancode sc = SDL_SCANCODE_UNKNOWN;
|
||||
bool down = false;
|
||||
if (ev.type == SDL_MOUSEBUTTONDOWN || ev.type == SDL_MOUSEBUTTONUP) {
|
||||
if (ev.button.button >= 1 && ev.button.button <= 5)
|
||||
sc = PSXKB_MOUSE_SC(ev.button.button);
|
||||
down = ev.type == SDL_MOUSEBUTTONDOWN;
|
||||
} else if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
|
||||
#if defined(PSX_SDL3)
|
||||
sc = ev.key.scancode;
|
||||
#else
|
||||
sc = ev.key.keysym.scancode;
|
||||
#endif
|
||||
down = ev.type == SDL_KEYDOWN;
|
||||
}
|
||||
const bool swallowed = psx_keybinds_control_suppressed(1, sc) != 0;
|
||||
if (!down) psx_keybinds_release_control(1, sc);
|
||||
return swallowed;
|
||||
}
|
||||
const uint64_t now = now_ns(), t = event_ns(ev, now);
|
||||
update(now);
|
||||
#if defined(PSX_SDL3)
|
||||
if (ev.type == SDL_EVENT_WINDOW_FOCUS_LOST || ev.type == SDL_EVENT_WINDOW_HIDDEN ||
|
||||
ev.type == SDL_EVENT_WINDOW_MINIMIZED) {
|
||||
if (window_ && ev.window.windowID == SDL_GetWindowID(window_)) state().reset();
|
||||
} else if (ev.type == SDL_EVENT_MOUSE_REMOVED) {
|
||||
state().reset();
|
||||
}
|
||||
#else
|
||||
if (window_ && ev.type == SDL_WINDOWEVENT && ev.window.windowID == SDL_GetWindowID(window_) &&
|
||||
(ev.window.event == SDL_WINDOWEVENT_FOCUS_LOST || ev.window.event == SDL_WINDOWEVENT_HIDDEN ||
|
||||
ev.window.event == SDL_WINDOWEVENT_MINIMIZED)) state().reset();
|
||||
#endif
|
||||
if (ev.type == SDL_MOUSEMOTION) {
|
||||
if (!window_ || ev.motion.windowID != SDL_GetWindowID(window_)) return false;
|
||||
if (ev.motion.which == SDL_TOUCH_MOUSEID) return false;
|
||||
#if defined(PSX_SDL3)
|
||||
if (ev.motion.which == SDL_PEN_MOUSEID) return false;
|
||||
#endif
|
||||
state().motion(t, now, ev.motion.xrel, ev.motion.yrel);
|
||||
} else if (ev.type == SDL_MOUSEBUTTONDOWN || ev.type == SDL_MOUSEBUTTONUP) {
|
||||
if (ev.button.which == SDL_TOUCH_MOUSEID) return false;
|
||||
#if defined(PSX_SDL3)
|
||||
if (ev.button.which == SDL_PEN_MOUSEID) return false;
|
||||
#endif
|
||||
// An owned activation release stays immediate even outside the game
|
||||
// window. Only a fresh down in this window may acquire capture.
|
||||
if (!window_ || (ev.button.windowID != SDL_GetWindowID(window_) &&
|
||||
ev.type != SDL_MOUSEBUTTONUP)) return false;
|
||||
if (ev.button.button >= 1 && ev.button.button <= 5)
|
||||
return control(t, now, psx::MouseControl(ev.button.button), ev.type == SDL_MOUSEBUTTONDOWN, false);
|
||||
} else if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
|
||||
#if defined(PSX_SDL3)
|
||||
if (!window_ || ev.key.windowID != SDL_GetWindowID(window_)) return false;
|
||||
const auto sc = ev.key.scancode;
|
||||
#else
|
||||
if (!window_ || ev.key.windowID != SDL_GetWindowID(window_)) return false;
|
||||
const auto sc = ev.key.keysym.scancode;
|
||||
#endif
|
||||
const auto c = sc == SDL_SCANCODE_ESCAPE ? psx::MouseControl::Escape :
|
||||
sc == SDL_SCANCODE_LALT ? psx::MouseControl::LeftAlt : psx::MouseControl::None;
|
||||
if (c != psx::MouseControl::None)
|
||||
return control(t, now, c, ev.type == SDL_KEYDOWN, ev.key.repeat != 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void psx_local_mouse_pad(bool connected, bool analog, uint16_t buttons, uint8_t& rx, uint8_t& ry) {
|
||||
if (!state().installed()) return;
|
||||
host_.connected = connected;
|
||||
host_.analog = analog;
|
||||
host_.native_right = rx != 128 || ry != 128;
|
||||
host_.start = (buttons & 0x0008) == 0;
|
||||
host_.buttons = buttons;
|
||||
const uint64_t now = now_ns();
|
||||
update(now);
|
||||
state().sample(now, rx, ry);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "psx_sdl.h"
|
||||
|
||||
/* All calls, including reset/release, belong to the SDL owner thread. */
|
||||
void psx_local_mouse_clear();
|
||||
bool psx_local_mouse_installed();
|
||||
void psx_local_mouse_reset();
|
||||
void psx_local_mouse_begin(SDL_Window* window, bool live);
|
||||
bool psx_local_mouse_event(const SDL_Event& event);
|
||||
void psx_local_mouse_pad(bool connected, bool analog, uint16_t buttons,
|
||||
uint8_t& rx, uint8_t& ry);
|
||||
+265
-187
@@ -32,6 +32,8 @@
|
||||
#include "psx_rewind.h"
|
||||
#include "psx_savestate_menu.h"
|
||||
#include "host_osd.h"
|
||||
#include "local_mouse_sdl.h"
|
||||
#include "pad_external_input.h"
|
||||
#include "host_keymap.h"
|
||||
#include "png_write.h" /* png_write_rgb — present_shot readback */
|
||||
#include "overlay_capture.h"
|
||||
@@ -1054,6 +1056,7 @@ extern "C" void psx_frontend_on_savestate_refused(int is_load, int slot,
|
||||
|
||||
extern "C" void psx_frontend_on_savestate_loaded(void) {
|
||||
psx_projection_reset_session();
|
||||
psx_local_mouse_reset();
|
||||
mod_runtime_on_savestate_loaded();
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_note_savestate_loaded();
|
||||
@@ -1557,6 +1560,7 @@ extern "C" int psx_mod_set_adaptive_display_aspect(
|
||||
static PSXModSessionBaseline g_mod_owned_baseline;
|
||||
|
||||
static void reset_mod_owned_presentation(void) {
|
||||
psx_local_mouse_clear();
|
||||
PSXModSessionScalars live;
|
||||
live.video_vsync = g_video_vsync;
|
||||
live.frame_interpolation = g_frame_interpolation;
|
||||
@@ -3710,6 +3714,7 @@ static const char* netplay_start_failure(int nrc, const PsxNetplayConfig& cfg) {
|
||||
}
|
||||
|
||||
static void shutdown_runtime(void) {
|
||||
psx_local_mouse_clear();
|
||||
/* (sljit removed 2026-07-15: overlay_compile_worker_stop joined the
|
||||
* off-thread JIT worker here; the worker no longer exists.) */
|
||||
psx_netplay_shutdown();
|
||||
@@ -5083,6 +5088,7 @@ static int assert_sio_pad_profile(int s, bool dev_here) {
|
||||
* While delay-sync netplay is active, SIO connection/type are owned by
|
||||
* psx_netplay (session slots stay plugged); only refresh host SDL handles. */
|
||||
static void refresh_player_devices(void) {
|
||||
psx_local_mouse_reset();
|
||||
const int netplay = psx_netplay_active();
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
PlayerInput& p = g_players[s];
|
||||
@@ -5583,6 +5589,7 @@ static void dev_any_controller_sticks(uint8_t st[4]) {
|
||||
}
|
||||
|
||||
static void savestate_input_guard_arm(void) {
|
||||
psx_local_mouse_reset();
|
||||
uint32_t now = (uint32_t)SDL_GetTicks();
|
||||
g_savestate_input_guard_min_until = now + 90u;
|
||||
g_savestate_input_guard_max_until = now + 700u;
|
||||
@@ -5694,9 +5701,12 @@ static void apply_input_override_to_sio(int override_word) {
|
||||
(uint8_t)(eff_analog ? 1 : 0));
|
||||
}
|
||||
|
||||
/* Capture one SIO slot's host pad into a netplay/local blob. Returns 1 if a
|
||||
* device (or dev-any P1) is present; 0 leaves *out as released/disconnected. */
|
||||
static int capture_pad_slot(int s, PsxNetPad* out) {
|
||||
/* Capture one SIO slot's PHYSICAL host pad into a netplay/local blob. Returns
|
||||
* 1 if a device (or dev-any P1) is present; 0 leaves *out as released/
|
||||
* disconnected. `guarded` (savestate input guard) delivers neutral buttons and
|
||||
* sticks but still resolves presence/type. No mouse or source side effects:
|
||||
* those are layered on top by pad_ext_resolve(). */
|
||||
static int capture_pad_slot(int s, PsxNetPad* out, bool guarded) {
|
||||
if (!out) return 0;
|
||||
out->buttons = 0xFFFFu;
|
||||
out->lx = out->ly = out->rx = out->ry = 0x80u;
|
||||
@@ -5733,7 +5743,7 @@ static int capture_pad_slot(int s, PsxNetPad* out) {
|
||||
s, player, mode, src, p, policy_buttons, st);
|
||||
const int eff_analog =
|
||||
effective_mode == PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
|
||||
if (savestate_input_guard_active()) {
|
||||
if (guarded) {
|
||||
out->buttons = 0xFFFFu;
|
||||
out->lx = out->ly = out->rx = out->ry = 0x80u;
|
||||
out->analog = eff_analog ? 1u : 0u;
|
||||
@@ -6298,6 +6308,80 @@ done:
|
||||
freeze_heartbeat_set_paused(0);
|
||||
}
|
||||
|
||||
/* Main-thread implementation of the PadExtHooks seams (see
|
||||
* pad_external_input.h for the resolution order and precedence rule). */
|
||||
static int pad_ext_guard_active(void*) { return savestate_input_guard_active(); }
|
||||
static int pad_ext_capture_local(void*, int s, PsxNetPad* out, int guarded) {
|
||||
return capture_pad_slot(s, out, guarded != 0);
|
||||
}
|
||||
static int pad_ext_source_sample(void*, int s, PSXModControllerState* st, int* released) {
|
||||
return mod_controller_source_sample((uint32_t)s, st, released);
|
||||
}
|
||||
static void pad_ext_source_release(void*, int s, PsxNetPad* pad) {
|
||||
/* Source detached/reset: deliver one neutral frame (even on a port with no
|
||||
* device) and give the port back its real SIO connection/config state; a
|
||||
* device resumes next frame. */
|
||||
const bool dev_here = (dev_any_input_enabled() && s == 0);
|
||||
const int boot_mode = assert_sio_pad_profile(s, dev_here);
|
||||
pad->buttons = 0xffff;
|
||||
pad->lx = pad->ly = pad->rx = pad->ry = 128;
|
||||
pad->analog = pad_mode_boot_analog(boot_mode) ? 1 : 0;
|
||||
pad->connected = 1;
|
||||
}
|
||||
static void pad_ext_source_resolve(void*, int s, const PSXModControllerState* source,
|
||||
int guarded, PsxNetPad* pad) {
|
||||
const bool dev_here = (dev_any_input_enabled() && s == 0);
|
||||
pad->lx = (uint8_t)source->lx; pad->ly = (uint8_t)source->ly;
|
||||
pad->rx = (uint8_t)source->rx; pad->ry = (uint8_t)source->ry;
|
||||
pad->connected = 1;
|
||||
/* Presentation goes through the same resolution as a physical pad: the
|
||||
* source's `analog` is the device capability, then mod mode override,
|
||||
* multitap rule and presentation policy. */
|
||||
const PlayerInput& p = g_players[s];
|
||||
int mode = effective_player_mode_for_sio(p, s);
|
||||
if (g_mod_controller_mode_override[s] >= 0 &&
|
||||
!(sio_pad_on_multitap(s) && !sio_get_multitap_analog()))
|
||||
mode = g_mod_controller_mode_override[s];
|
||||
if (!source->analog) mode = PSXRecompV4::PAD_MODE_DIGITAL;
|
||||
uint8_t st[4] = { pad->lx, pad->ly, pad->rx, pad->ry };
|
||||
int eff_mode = controller_policy_resolve_mode(
|
||||
s, s + 1, mode, pad_sources_for(p, dev_here), p, pad->buttons, st);
|
||||
if (!source->analog ||
|
||||
(sio_pad_on_multitap(s) && !sio_get_multitap_analog()))
|
||||
eff_mode = PSXRecompV4::PAD_MODE_DIGITAL;
|
||||
pad->analog = eff_mode == PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
|
||||
if (!pad->analog)
|
||||
pad->lx = pad->ly = pad->rx = pad->ry = 128;
|
||||
if (guarded) {
|
||||
pad->buttons = 0xffff;
|
||||
pad->lx = pad->ly = pad->rx = pad->ry = 128;
|
||||
}
|
||||
/* The source occupies the port; config capability follows the same rule
|
||||
* as a physical pad (apply_pad_slot_to_sio and the SIO layer keep multitap
|
||||
* taps digital). */
|
||||
const ModControllerPresentationPolicy& policy = g_mod_controller_policy[s];
|
||||
sio_set_pad_connected(s, 1);
|
||||
sio_set_pad_config_capable(
|
||||
s, policy.callback ? policy.config_capable
|
||||
: mode != PSXRecompV4::PAD_MODE_DIGITAL);
|
||||
}
|
||||
static void pad_ext_mouse_reset(void*) { psx_local_mouse_reset(); }
|
||||
static void pad_ext_mouse_fold(void*, int connected, int analog, uint16_t buttons,
|
||||
uint8_t* rx, uint8_t* ry) {
|
||||
psx_local_mouse_pad(connected != 0, analog != 0, buttons, *rx, *ry);
|
||||
}
|
||||
static PadExtHooks pad_ext_main_hooks(void) {
|
||||
PadExtHooks h{};
|
||||
h.guard_active = pad_ext_guard_active;
|
||||
h.capture_local = pad_ext_capture_local;
|
||||
h.source_sample = pad_ext_source_sample;
|
||||
h.source_release = pad_ext_source_release;
|
||||
h.source_resolve = pad_ext_source_resolve;
|
||||
h.mouse_reset = pad_ext_mouse_reset;
|
||||
h.mouse_fold = pad_ext_mouse_fold;
|
||||
return h;
|
||||
}
|
||||
|
||||
static void sample_pad_into_sio(int override) {
|
||||
/* Selfcheck fighter mash owns P1 when enabled (headless-safe). */
|
||||
if (override < 0) {
|
||||
@@ -6306,6 +6390,7 @@ static void sample_pad_into_sio(int override) {
|
||||
override = (int)mash;
|
||||
}
|
||||
if (override >= 0) {
|
||||
psx_local_mouse_reset();
|
||||
apply_input_override_to_sio(override);
|
||||
return;
|
||||
}
|
||||
@@ -6314,67 +6399,10 @@ static void sample_pad_into_sio(int override) {
|
||||
if (n > PSX_MAX_PLAYERS) n = PSX_MAX_PLAYERS;
|
||||
const uint32_t consumer_sim =
|
||||
psx_start_consumer_enabled() ? psx_start_consumer_offline_frame() : 0u;
|
||||
PadExtHooks hooks = pad_ext_main_hooks();
|
||||
for (int s = 0; s < n; s++) {
|
||||
PsxNetPad pad;
|
||||
PSXModControllerState source;
|
||||
int released = 0;
|
||||
if (mod_controller_source_sample((uint32_t)s, &source, &released)) {
|
||||
const bool dev_here = (dev_any_input_enabled() && s == 0);
|
||||
if (released) {
|
||||
/* Source detached/reset: deliver one neutral frame (even on a
|
||||
* port with no device) and give the port back its real SIO
|
||||
* connection/config state; a device resumes next frame. */
|
||||
const int boot_mode = assert_sio_pad_profile(s, dev_here);
|
||||
pad.buttons = 0xffff;
|
||||
pad.lx = pad.ly = pad.rx = pad.ry = 128;
|
||||
pad.analog = pad_mode_boot_analog(boot_mode) ? 1 : 0;
|
||||
pad.connected = 1;
|
||||
} else {
|
||||
/* Sample physical buttons separately; its axes/type cannot
|
||||
* steer the source's virtual pad. Guards release every source
|
||||
* channel. */
|
||||
PsxNetPad local;
|
||||
const int have_local = capture_pad_slot(s, &local);
|
||||
pad.buttons = (uint16_t)source.buttons;
|
||||
if (have_local) pad.buttons &= local.buttons;
|
||||
pad.lx = (uint8_t)source.lx; pad.ly = (uint8_t)source.ly;
|
||||
pad.rx = (uint8_t)source.rx; pad.ry = (uint8_t)source.ry;
|
||||
pad.connected = 1;
|
||||
/* Presentation goes through the same resolution as a physical
|
||||
* pad: the source's `analog` is the device capability, then
|
||||
* mod mode override, multitap rule and presentation policy. */
|
||||
const PlayerInput& p = g_players[s];
|
||||
int mode = effective_player_mode_for_sio(p, s);
|
||||
if (g_mod_controller_mode_override[s] >= 0 &&
|
||||
!(sio_pad_on_multitap(s) && !sio_get_multitap_analog()))
|
||||
mode = g_mod_controller_mode_override[s];
|
||||
if (!source.analog) mode = PSXRecompV4::PAD_MODE_DIGITAL;
|
||||
uint8_t st[4] = { pad.lx, pad.ly, pad.rx, pad.ry };
|
||||
int eff_mode = controller_policy_resolve_mode(
|
||||
s, s + 1, mode, pad_sources_for(p, dev_here), p,
|
||||
pad.buttons, st);
|
||||
if (!source.analog ||
|
||||
(sio_pad_on_multitap(s) && !sio_get_multitap_analog()))
|
||||
eff_mode = PSXRecompV4::PAD_MODE_DIGITAL;
|
||||
pad.analog = eff_mode == PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
|
||||
if (!pad.analog)
|
||||
pad.lx = pad.ly = pad.rx = pad.ry = 128;
|
||||
if (savestate_input_guard_active()) {
|
||||
pad.buttons = 0xffff;
|
||||
pad.lx = pad.ly = pad.rx = pad.ry = 128;
|
||||
}
|
||||
/* The source occupies the port; config capability follows the
|
||||
* same rule as a physical pad (apply_pad_slot_to_sio and the
|
||||
* SIO layer keep multitap taps digital). */
|
||||
const ModControllerPresentationPolicy& policy =
|
||||
g_mod_controller_policy[s];
|
||||
sio_set_pad_connected(s, 1);
|
||||
sio_set_pad_config_capable(
|
||||
s, policy.callback
|
||||
? policy.config_capable
|
||||
: mode != PSXRecompV4::PAD_MODE_DIGITAL);
|
||||
}
|
||||
} else if (!capture_pad_slot(s, &pad)) continue; /* no device in this port */
|
||||
if (!pad_ext_resolve(&hooks, s, &pad)) continue; /* no device in this port */
|
||||
/* Push sticks every frame; request the pad type (digital/analog) through
|
||||
* the coherent channel so a policy switch is applied only at an idle,
|
||||
* non-config bus boundary (never mid-poll / mid-handshake). This is the
|
||||
@@ -6844,6 +6872,7 @@ static void savestate_menu_close(void) {
|
||||
}
|
||||
|
||||
static void savestate_menu_toggle(SDL_Keycode opened_by_key) {
|
||||
psx_local_mouse_reset();
|
||||
if (psx_rewind_is_open())
|
||||
return;
|
||||
if (savestate_menu_open) {
|
||||
@@ -7007,8 +7036,10 @@ static int rewind_toggle_buttons_down(void) {
|
||||
static void rewind_poll_toggle_buttons(void) {
|
||||
static int was_down;
|
||||
int down = rewind_toggle_buttons_down();
|
||||
if (down && !was_down && !psx_rewind_is_open())
|
||||
if (down && !was_down && !psx_rewind_is_open()) {
|
||||
psx_local_mouse_reset();
|
||||
psx_rewind_toggle();
|
||||
}
|
||||
was_down = down;
|
||||
}
|
||||
|
||||
@@ -7113,10 +7144,13 @@ static void rewind_pause_present(void) {
|
||||
|
||||
/* Freeze guest in vblank present while the rewind filmstrip is open. */
|
||||
static void rewind_host_pause_loop(void) {
|
||||
psx_local_mouse_begin(sdl_window, false);
|
||||
psx_local_mouse_reset();
|
||||
freeze_heartbeat_set_paused(1);
|
||||
while (psx_rewind_is_open()) {
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
(void)psx_local_mouse_event(ev);
|
||||
if (ev.type == SDL_QUIT) {
|
||||
psx_crash_trace_set_exit_origin("sdl_window_close");
|
||||
shutdown_runtime();
|
||||
@@ -7157,10 +7191,13 @@ static void rewind_host_pause_loop(void) {
|
||||
|
||||
/* Freeze guest in vblank present while the save-state slot menu is open. */
|
||||
static void savestate_menu_host_pause_loop(void) {
|
||||
psx_local_mouse_begin(sdl_window, false);
|
||||
psx_local_mouse_reset();
|
||||
freeze_heartbeat_set_paused(1);
|
||||
while (savestate_menu_open) {
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
(void)psx_local_mouse_event(ev);
|
||||
if (ev.type == SDL_QUIT) {
|
||||
psx_crash_trace_set_exit_origin("sdl_window_close");
|
||||
shutdown_runtime();
|
||||
@@ -7253,6 +7290,154 @@ static void headless_present_image_ring_capture(void) {
|
||||
present_image_ring_push_argb((uint32_t)s_frame_count, buf.data(), w, h, w);
|
||||
}
|
||||
#endif
|
||||
// Shared by early and post-pacer sampling: do not PumpEvents without draining
|
||||
// ordered motion/control events and the existing hotkeys before folding binds.
|
||||
static bool drain_host_events() {
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (psx_local_mouse_event(ev)) continue;
|
||||
if (ev.type == SDL_QUIT) {
|
||||
if (psx_netplay_active()) {
|
||||
netplay_soft_exit("sdl_window_close");
|
||||
return false;
|
||||
}
|
||||
psx_crash_trace_set_exit_origin("sdl_window_close");
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
|
||||
refresh_player_devices();
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
bool ours = false;
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
#if defined(PSX_SDL3)
|
||||
if (ev.gdevice.which == g_players[s].instance) { ours = true; break; }
|
||||
#else
|
||||
if (ev.cdevice.which == g_players[s].instance) { ours = true; break; }
|
||||
#endif
|
||||
}
|
||||
if (ours) {
|
||||
close_controller();
|
||||
refresh_player_devices();
|
||||
}
|
||||
} else if (ev.type == SDL_KEYDOWN) {
|
||||
#if defined(PSX_SDL3)
|
||||
const SDL_Keymod mod = ev.key.mod;
|
||||
const SDL_Keycode key = ev.key.key;
|
||||
const SDL_Scancode scancode = ev.key.scancode;
|
||||
const int key_repeat = ev.key.repeat ? 1 : 0;
|
||||
#else
|
||||
const Uint16 mod = ev.key.keysym.mod;
|
||||
const SDL_Keycode key = ev.key.keysym.sym;
|
||||
const SDL_Scancode scancode = ev.key.keysym.scancode;
|
||||
const int key_repeat = ev.key.repeat ? 1 : 0;
|
||||
#endif
|
||||
if (key == SDLK_ESCAPE && psx_netplay_active()) {
|
||||
netplay_soft_exit("netplay_escape");
|
||||
return false;
|
||||
}
|
||||
if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_REWIND, (int)key,
|
||||
(int)scancode, (int)mod)) {
|
||||
psx_local_mouse_reset();
|
||||
psx_rewind_toggle();
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_SAVE_STATE_MENU,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
psx_local_mouse_reset();
|
||||
savestate_menu_toggle(key);
|
||||
}
|
||||
else if (key == SDLK_c && (mod & KMOD_CTRL)) {
|
||||
std::fprintf(stdout, "[DEBUG] Forzando reinserción de CD...\n");
|
||||
debug_force_cd_reinsert();
|
||||
host_osd_push("CD reinsert", 1500);
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_TURBO_TOGGLE,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
fast_forward_toggle_flip();
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_CAPTURE_MARK,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_capture_mark();
|
||||
host_osd_push("Capture marked (debug rings frozen)", 2000);
|
||||
#else
|
||||
host_osd_push("Capture mark needs a debug-tools build", 2000);
|
||||
#endif
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_DISPLAY_PERF,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
fps_telemetry_toggle();
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_SCANLINES,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
psx_video_set_scanlines(g_video_scanlines ? 0 : 1,
|
||||
g_video_scanline_strength);
|
||||
char msg[48];
|
||||
std::snprintf(msg, sizeof(msg), "Scanlines %s",
|
||||
g_video_scanlines ? "on" : "off");
|
||||
host_osd_push(msg, 1200);
|
||||
}
|
||||
/* Host volume: config.ini [KeyMap] VolumeUp/VolumeDown
|
||||
* (defaults: keypad +/-). 5% steps; shows right-side bar. */
|
||||
else if (host_keymap_match_event(HOST_KEYMAP_VOLUME_UP,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
host_volume_adjust(+5);
|
||||
} else if (host_keymap_match_event(HOST_KEYMAP_VOLUME_DOWN,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
host_volume_adjust(-5);
|
||||
}
|
||||
/* Fullscreen toggle: Alt+Enter or Cmd/Ctrl+F. Toggles between
|
||||
* windowed and the CONFIGURED tri-state mode (g_fullscreen: 1
|
||||
* borderless desktop fullscreen keeping the desktop resolution
|
||||
* and letterboxing the image, or 2 exclusive fullscreen — a
|
||||
* real display-mode change). Track the selected live mode:
|
||||
* borderless deliberately leaves SDL's fullscreen bit clear. */
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_FULLSCREEN,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
const int target = s_window_fullscreen.mode ? 0 :
|
||||
(g_fullscreen ? g_fullscreen : 1);
|
||||
if (psx_window_fullscreen_set(sdl_window, &s_window_fullscreen, target) == 0)
|
||||
host_osd_push(target ? "Fullscreen" : "Windowed", 1500);
|
||||
else
|
||||
host_osd_push("Fullscreen change failed", 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool local_mouse_live(int override_word) {
|
||||
PadExtGate g{};
|
||||
g.injected_input = override_word >= 0;
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
uint8_t axes[4];
|
||||
g.injected_input = g.injected_input || debug_server_get_axis_override(axes) != 0;
|
||||
#endif
|
||||
g.headless = g_headless;
|
||||
g.netplay_active = psx_netplay_active();
|
||||
g.netplay_resim = psx_netplay_is_resimulating();
|
||||
g.selfcheck_locked = psx_selfcheck_input_locked();
|
||||
g.selfcheck_resim = psx_selfcheck_resim_active();
|
||||
g.render_pass = g_psx_render_pass_active;
|
||||
g.savestate_menu_open = savestate_menu_open;
|
||||
g.rewind_open = psx_rewind_is_open();
|
||||
g.input_guard = savestate_input_guard_active();
|
||||
return pad_ext_live(&g) != 0;
|
||||
}
|
||||
|
||||
static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
NetplayVblankEpilogue ep{};
|
||||
@@ -7288,6 +7473,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
int override = -1;
|
||||
#endif
|
||||
|
||||
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
|
||||
{
|
||||
/* Outside every debug guard on purpose: production must be measurable. */
|
||||
extern uint64_t s_frame_count;
|
||||
@@ -7410,127 +7596,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
|
||||
if (!g_headless) {
|
||||
/* Pump SDL events to prevent window freeze. */
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (ev.type == SDL_QUIT) {
|
||||
if (psx_netplay_active()) {
|
||||
netplay_soft_exit("sdl_window_close");
|
||||
return ep;
|
||||
}
|
||||
psx_crash_trace_set_exit_origin("sdl_window_close");
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
|
||||
refresh_player_devices();
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
bool ours = false;
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
#if defined(PSX_SDL3)
|
||||
if (ev.gdevice.which == g_players[s].instance) { ours = true; break; }
|
||||
#else
|
||||
if (ev.cdevice.which == g_players[s].instance) { ours = true; break; }
|
||||
#endif
|
||||
}
|
||||
if (ours) {
|
||||
close_controller();
|
||||
refresh_player_devices();
|
||||
}
|
||||
} else if (ev.type == SDL_KEYDOWN) {
|
||||
#if defined(PSX_SDL3)
|
||||
const SDL_Keymod mod = ev.key.mod;
|
||||
const SDL_Keycode key = ev.key.key;
|
||||
const SDL_Scancode scancode = ev.key.scancode;
|
||||
const int key_repeat = ev.key.repeat ? 1 : 0;
|
||||
#else
|
||||
const Uint16 mod = ev.key.keysym.mod;
|
||||
const SDL_Keycode key = ev.key.keysym.sym;
|
||||
const SDL_Scancode scancode = ev.key.keysym.scancode;
|
||||
const int key_repeat = ev.key.repeat ? 1 : 0;
|
||||
#endif
|
||||
if (key == SDLK_ESCAPE && psx_netplay_active()) {
|
||||
netplay_soft_exit("netplay_escape");
|
||||
return ep;
|
||||
}
|
||||
if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_REWIND, (int)key,
|
||||
(int)scancode, (int)mod)) {
|
||||
psx_rewind_toggle();
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_SAVE_STATE_MENU,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
savestate_menu_toggle(key);
|
||||
}
|
||||
else if (key == SDLK_c && (mod & KMOD_CTRL)) {
|
||||
std::fprintf(stdout, "[DEBUG] Forzando reinserción de CD...\n");
|
||||
debug_force_cd_reinsert();
|
||||
host_osd_push("CD reinsert", 1500);
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_TURBO_TOGGLE,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
fast_forward_toggle_flip();
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_CAPTURE_MARK,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_capture_mark();
|
||||
host_osd_push("Capture marked (debug rings frozen)", 2000);
|
||||
#else
|
||||
host_osd_push("Capture mark needs a debug-tools build", 2000);
|
||||
#endif
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_DISPLAY_PERF,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
fps_telemetry_toggle();
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_SCANLINES,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
psx_video_set_scanlines(g_video_scanlines ? 0 : 1,
|
||||
g_video_scanline_strength);
|
||||
char msg[48];
|
||||
std::snprintf(msg, sizeof(msg), "Scanlines %s",
|
||||
g_video_scanlines ? "on" : "off");
|
||||
host_osd_push(msg, 1200);
|
||||
}
|
||||
/* Host volume: config.ini [KeyMap] VolumeUp/VolumeDown
|
||||
* (defaults: keypad +/-). 5% steps; shows right-side bar. */
|
||||
else if (host_keymap_match_event(HOST_KEYMAP_VOLUME_UP,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
host_volume_adjust(+5);
|
||||
} else if (host_keymap_match_event(HOST_KEYMAP_VOLUME_DOWN,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
host_volume_adjust(-5);
|
||||
}
|
||||
/* Fullscreen toggle: Alt+Enter or Cmd/Ctrl+F. Toggles between
|
||||
* windowed and the CONFIGURED tri-state mode (g_fullscreen: 1
|
||||
* borderless desktop fullscreen keeping the desktop resolution
|
||||
* and letterboxing the image, or 2 exclusive fullscreen — a
|
||||
* real display-mode change). Track the selected live mode:
|
||||
* borderless deliberately leaves SDL's fullscreen bit clear. */
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match_event(HOST_KEYMAP_FULLSCREEN,
|
||||
(int)key, (int)scancode,
|
||||
(int)mod)) {
|
||||
const int target = s_window_fullscreen.mode ? 0 :
|
||||
(g_fullscreen ? g_fullscreen : 1);
|
||||
if (psx_window_fullscreen_set(sdl_window, &s_window_fullscreen, target) == 0)
|
||||
host_osd_push(target ? "Fullscreen" : "Windowed", 1500);
|
||||
else
|
||||
host_osd_push("Fullscreen change failed", 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
|
||||
if (!drain_host_events()) return ep;
|
||||
savestate_menu_poll_toggle_buttons();
|
||||
rewind_poll_toggle_buttons();
|
||||
fast_forward_toggle_poll_buttons();
|
||||
@@ -7770,8 +7837,10 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* Rewind / Save states so the launcher's binding editor covers it.
|
||||
* g_manual_turbo_latched is the press-to-lock twin (TurboToggle /
|
||||
* fast_forward_toggle_pad) and drives the same path. */
|
||||
uint8_t host_keys[SDL_NUM_SCANCODES];
|
||||
psx_keybinds_host_keys(keys, host_keys);
|
||||
const bool kb_turbo = host_hotkey_input_focused() &&
|
||||
host_keymap_down(HOST_KEYMAP_TURBO, keys, (int)SDL_GetModState());
|
||||
host_keymap_down(HOST_KEYMAP_TURBO, host_keys, (int)SDL_GetModState());
|
||||
if (kb_turbo || g_manual_turbo_latched ||
|
||||
hotkey_pad_binding_down(g_hotkey_pad_fast_forward)) {
|
||||
const int mult = manual_fast_forward_multiplier();
|
||||
@@ -7901,7 +7970,16 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* semantics) — the mid-frame re-sample would fork the resim. */
|
||||
if (g_low_latency_input && !psx_selfcheck_input_locked()) {
|
||||
SDL_GameControllerUpdate(); /* refresh pad state after the wait */
|
||||
SDL_PumpEvents(); /* refresh keyboard state */
|
||||
if (psx_local_mouse_installed()) {
|
||||
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
|
||||
if (!drain_host_events()) return ep;
|
||||
// A hotkey acquired during pacing may open a host menu.
|
||||
if (savestate_menu_open) savestate_menu_host_pause_loop();
|
||||
if (psx_rewind_is_open()) rewind_host_pause_loop();
|
||||
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
|
||||
} else {
|
||||
SDL_PumpEvents(); // retain native timing when no policy exists
|
||||
}
|
||||
sample_pad_into_sio(override);
|
||||
latency_ring_restamp_input();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "pad_external_input.h"
|
||||
|
||||
int pad_ext_live(const PadExtGate *g) {
|
||||
return g && !g->injected_input && !g->headless && !g->netplay_active &&
|
||||
!g->netplay_resim && !g->selfcheck_locked && !g->selfcheck_resim &&
|
||||
!g->render_pass && !g->savestate_menu_open && !g->rewind_open &&
|
||||
!g->input_guard;
|
||||
}
|
||||
|
||||
int pad_ext_resolve(const PadExtHooks *h, int s, PsxNetPad *out) {
|
||||
PSXModControllerState src;
|
||||
int released = 0;
|
||||
const int guarded = h->guard_active ? h->guard_active(h->ctx) : 0;
|
||||
const int has_source = h->source_sample &&
|
||||
h->source_sample(h->ctx, s, &src, &released);
|
||||
|
||||
if (has_source && released) {
|
||||
h->source_release(h->ctx, s, out);
|
||||
if (s == 0) h->mouse_reset(h->ctx);
|
||||
return 1;
|
||||
}
|
||||
if (has_source) {
|
||||
PsxNetPad local;
|
||||
const int have_local = h->capture_local(h->ctx, s, &local, guarded);
|
||||
out->buttons = (uint16_t)src.buttons;
|
||||
if (have_local) out->buttons &= local.buttons;
|
||||
h->source_resolve(h->ctx, s, &src, guarded, out);
|
||||
} else if (!h->capture_local(h->ctx, s, out, guarded)) {
|
||||
if (s == 0) h->mouse_reset(h->ctx);
|
||||
return 0;
|
||||
}
|
||||
if (s == 0) {
|
||||
if (guarded) h->mouse_reset(h->ctx);
|
||||
else h->mouse_fold(h->ctx, out->connected, out->analog, out->buttons,
|
||||
&out->rx, &out->ry);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
/* Ordered external-input resolution for one offline player's pad.
|
||||
*
|
||||
* (1) physical/local capture -> buttons, device presence
|
||||
* (2) offline controller source (psx_mod_set_controller_source), if any:
|
||||
* buttons = source & physical; sticks/type from the source, through
|
||||
* the controller presentation policy (hook)
|
||||
* (3) local mouse policy (psx_mod_set_local_mouse_policy), P1 only: may
|
||||
* override ONLY the right analog axes of whatever (1)/(2) produced.
|
||||
*
|
||||
* Precedence: a source never suppresses the mouse policy. The mouse sees the
|
||||
* RESOLVED pad (final buttons, final analog flag, final right stick), so a
|
||||
* capture is never taken and then discarded, and an active source whose right
|
||||
* stick is deflected, whose pad is digital, or whose Start is held disables
|
||||
* the mouse exactly as a physical stick/digital pad/Start would. The mouse
|
||||
* is reset (capture released) on a released/declined frame, no device, or an
|
||||
* armed savestate input guard.
|
||||
*
|
||||
* Pure: every side effect goes through PadExtHooks, so the order is testable
|
||||
* without SDL/SIO. Main (emulation) thread only. */
|
||||
#include <stdint.h>
|
||||
#include "mod_plugins.h"
|
||||
#include "psx_netplay.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct PadExtHooks {
|
||||
void *ctx;
|
||||
/* Input guard (savestate close/load): all external input delivers
|
||||
* neutral and the mouse is reset. Queried once per resolve. */
|
||||
int (*guard_active)(void *ctx);
|
||||
/* (1) Physical capture of port `s`; NO mouse side effects. Returns 1 if a
|
||||
* device is present, else 0 (*out released/disconnected). With
|
||||
* `guarded`, buttons/sticks are neutral but type/presence still resolve. */
|
||||
int (*capture_local)(void *ctx, int s, PsxNetPad *out, int guarded);
|
||||
/* (2) Source for port `s`: returns 0 when none; else fills *state and
|
||||
* *released (a one-frame neutral release after detach/reset). */
|
||||
int (*source_sample)(void *ctx, int s, PSXModControllerState *state, int *released);
|
||||
/* Release frame: neutral pad + the port's real SIO state restored. */
|
||||
void (*source_release)(void *ctx, int s, PsxNetPad *out);
|
||||
/* Live source frame: pad->buttons is already source&physical; fill
|
||||
* sticks/analog (presentation policy, multitap rule) and claim the port. */
|
||||
void (*source_resolve)(void *ctx, int s, const PSXModControllerState *state,
|
||||
int guarded, PsxNetPad *pad);
|
||||
/* (3) Mouse policy. Only P1 (s == 0) is ever offered to it. */
|
||||
void (*mouse_reset)(void *ctx);
|
||||
void (*mouse_fold)(void *ctx, int connected, int analog, uint16_t buttons,
|
||||
uint8_t *rx, uint8_t *ry);
|
||||
} PadExtHooks;
|
||||
|
||||
/* Resolve one port. Returns 1 = deliver *out, 0 = no device/source here. */
|
||||
int pad_ext_resolve(const PadExtHooks *h, int s, PsxNetPad *out);
|
||||
|
||||
/* Host conditions under which NO external input (mouse capture) may be live.
|
||||
* Mirrors the gates every other local-only feature obeys. 1 = live. */
|
||||
typedef struct PadExtGate {
|
||||
int injected_input; /* debug-server set_input / axis override */
|
||||
int headless;
|
||||
int netplay_active;
|
||||
int netplay_resim; /* rollback resimulation */
|
||||
int selfcheck_locked; /* selfcheck record/replay input lock */
|
||||
int selfcheck_resim;
|
||||
int render_pass; /* eye / extra render pass */
|
||||
int savestate_menu_open;
|
||||
int rewind_open;
|
||||
int input_guard; /* savestate input guard armed */
|
||||
} PadExtGate;
|
||||
int pad_ext_live(const PadExtGate *g);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -19,10 +19,6 @@
|
||||
* bind/save/load/rebind machinery as ordinary SDL_Scancode values while
|
||||
* held() resolves them against SDL_GetMouseState() instead of the keyboard
|
||||
* array. INI names: Mouse1 (left) .. Mouse5 (X2), plus LMB/RMB/MMB aliases. */
|
||||
#define PSXKB_MOUSE_SC_BASE 512 /* + SDL_BUTTON_* (1..5) */
|
||||
#define PSXKB_MOUSE_SC(btn) ((SDL_Scancode)(PSXKB_MOUSE_SC_BASE + (btn)))
|
||||
#define PSXKB_IS_MOUSE_SC(sc) \
|
||||
((int)(sc) > PSXKB_MOUSE_SC_BASE && (int)(sc) <= PSXKB_MOUSE_SC_BASE + 5)
|
||||
|
||||
/* PSX pad word bits (active-low), standard DualShock layout. Matches the
|
||||
* PAD_* masks in main.cpp / beetle_main.cpp. */
|
||||
@@ -350,12 +346,43 @@ static PsxPlayerBinds *player_binds(int player) {
|
||||
/* Is this single scancode (keyboard or mouse pseudo-code) currently held?
|
||||
* Mouse pseudo-scancodes MUST be checked before indexing keys[] — they sit
|
||||
* beyond the keyboard state array (SDL_NUM_SCANCODES entries). */
|
||||
static int held_sc(const uint8_t *keys, SDL_Scancode sc) {
|
||||
static uint8_t s_suppressed[PSXKB_MAX_PLAYERS][PSXKB_MOUSE_SC_BASE + 6];
|
||||
|
||||
void psx_keybinds_suppress_control(int player, SDL_Scancode sc) {
|
||||
if (player >= 1 && player <= PSXKB_MAX_PLAYERS && (int)sc > 0 &&
|
||||
(int)sc < PSXKB_MOUSE_SC_BASE + 6)
|
||||
s_suppressed[player - 1][(int)sc] = 1;
|
||||
}
|
||||
void psx_keybinds_release_control(int player, SDL_Scancode sc) {
|
||||
if (player >= 1 && player <= PSXKB_MAX_PLAYERS && (int)sc > 0 &&
|
||||
(int)sc < PSXKB_MOUSE_SC_BASE + 6)
|
||||
s_suppressed[player - 1][(int)sc] = 0;
|
||||
}
|
||||
int psx_keybinds_control_suppressed(int player, SDL_Scancode sc) {
|
||||
return player >= 1 && player <= PSXKB_MAX_PLAYERS && (int)sc > 0 &&
|
||||
(int)sc < PSXKB_MOUSE_SC_BASE + 6 && s_suppressed[player - 1][(int)sc];
|
||||
}
|
||||
void psx_keybinds_host_keys(const uint8_t *keys, uint8_t out[SDL_NUM_SCANCODES]) {
|
||||
if (!out) return;
|
||||
if (!keys) { memset(out, 0, SDL_NUM_SCANCODES); return; }
|
||||
memcpy(out, keys, SDL_NUM_SCANCODES);
|
||||
for (int sc = 1; sc < SDL_NUM_SCANCODES; ++sc)
|
||||
if (s_suppressed[0][sc]) out[sc] = 0;
|
||||
}
|
||||
static int held_sc(const uint8_t *keys, SDL_Scancode sc, int slot) {
|
||||
int down;
|
||||
if (PSXKB_IS_MOUSE_SC(sc)) {
|
||||
Uint32 m = SDL_GetMouseState(NULL, NULL);
|
||||
return (m & SDL_BUTTON((int)sc - PSXKB_MOUSE_SC_BASE)) != 0;
|
||||
down = (m & SDL_BUTTON((int)sc - PSXKB_MOUSE_SC_BASE)) != 0;
|
||||
} else {
|
||||
down = sc != SDL_SCANCODE_UNKNOWN && (int)sc >= 0 &&
|
||||
(int)sc < SDL_NUM_SCANCODES && keys[sc];
|
||||
}
|
||||
return sc != SDL_SCANCODE_UNKNOWN && (int)sc < SDL_NUM_SCANCODES && keys[sc];
|
||||
if (psx_keybinds_control_suppressed(slot + 1, sc)) {
|
||||
if (!down) psx_keybinds_release_control(slot + 1, sc);
|
||||
return 0;
|
||||
}
|
||||
return down;
|
||||
}
|
||||
|
||||
/* Is the input at button-def index i held for this player, via either its
|
||||
@@ -367,7 +394,7 @@ static int held(const uint8_t *keys, const PsxPlayerBinds *pb, int i) {
|
||||
const PsxPlayerBinds *alt = &s_alt_binds.player[slot];
|
||||
SDL_Scancode p = *(const SDL_Scancode *)((const char *)pb + s_buttons[i].offset);
|
||||
SDL_Scancode a = *(const SDL_Scancode *)((const char *)alt + s_buttons[i].offset);
|
||||
return held_sc(keys, p) || held_sc(keys, a);
|
||||
return held_sc(keys, p, (int)slot) || held_sc(keys, a, (int)slot);
|
||||
}
|
||||
|
||||
uint16_t psx_keybinds_pad_word(const uint8_t *keys, int player) {
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
#include "psx_keybinds.h"
|
||||
#include "host_keymap.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
static int failures;
|
||||
|
||||
@@ -51,6 +58,84 @@ int main(int argc, char **argv) {
|
||||
expect_word("start re-pressed with select held", 0xFFF6u,
|
||||
psx_keybinds_pad_word(keys, 1));
|
||||
|
||||
/* Captured Escape must be swallowed before folding; a primary/alternate
|
||||
* binding and a simultaneously held controller action are independent. */
|
||||
memset(keys, 0, sizeof(keys));
|
||||
/* Rebinding intentionally moves a key from its previous input. The INI
|
||||
* loader permits shared sources, so construct this fixture through it. */
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (_mkdir("keyboard_pad_chord_fixture") != 0) return 1;
|
||||
#else
|
||||
if (mkdir("keyboard_pad_chord_fixture", 0755) != 0) return 1;
|
||||
#endif
|
||||
FILE *config = fopen("keyboard_pad_chord_fixture/keybinds.ini", "wb");
|
||||
if (!config) return 1;
|
||||
fputs("[player1]\ncross = Escape\ncircle = S, Escape\n"
|
||||
"[player2]\ncross = Escape\n", config);
|
||||
fclose(config);
|
||||
psx_keybinds_init("keyboard_pad_chord_fixture/fixture.exe");
|
||||
remove("keyboard_pad_chord_fixture/keybinds.ini");
|
||||
#ifdef _WIN32
|
||||
_rmdir("keyboard_pad_chord_fixture");
|
||||
#else
|
||||
rmdir("keyboard_pad_chord_fixture");
|
||||
#endif
|
||||
expect_scancode("shared INI primary Escape", SDL_SCANCODE_ESCAPE,
|
||||
psx_keybinds_get_button(1, PSX_KB_CROSS));
|
||||
expect_scancode("shared INI alternate Escape", SDL_SCANCODE_ESCAPE,
|
||||
psx_keybinds_get_button_alt(1, PSX_KB_CIRCLE));
|
||||
}
|
||||
{
|
||||
FILE *config = fopen("keyboard_consumed_host_test.ini", "wb");
|
||||
if (!config) return 1;
|
||||
fputs("[KeyMap]\nTurbo = Escape\n", config);
|
||||
fclose(config);
|
||||
host_keymap_load("keyboard_consumed_host_test.ini");
|
||||
remove("keyboard_consumed_host_test.ini");
|
||||
}
|
||||
keys[SDL_SCANCODE_ESCAPE] = 1;
|
||||
expect_word("configured Turbo=Escape is normally live", 1,
|
||||
(uint16_t)host_keymap_down(HOST_KEYMAP_TURBO, keys, 0));
|
||||
psx_keybinds_suppress_control(1, SDL_SCANCODE_ESCAPE);
|
||||
expect_word("Escape primary and alternate consumed", 0xFFFFu,
|
||||
psx_keybinds_pad_word(keys, 1));
|
||||
expect_word("held Escape remains consumed", 0xFFFFu,
|
||||
psx_keybinds_pad_word(keys, 1));
|
||||
for (int poll = 0; poll < 3; ++poll) {
|
||||
uint8_t host_keys[SDL_NUM_SCANCODES];
|
||||
psx_keybinds_host_keys(keys, host_keys);
|
||||
expect_word("captured Escape does not activate polled Turbo", 0,
|
||||
(uint16_t)host_keymap_down(HOST_KEYMAP_TURBO, host_keys, 0));
|
||||
expect_word("host filter preserves physical SDL state", 1, keys[SDL_SCANCODE_ESCAPE]);
|
||||
expect_word("simultaneous P2 Escape action stays live", 0xBFFFu,
|
||||
psx_keybinds_pad_word(keys, 2));
|
||||
}
|
||||
expect_word("controller Cross preserved through source merge", 0xBFFFu,
|
||||
(uint16_t)(0xBFFFu & psx_keybinds_pad_word(keys, 1)));
|
||||
keys[SDL_SCANCODE_ESCAPE] = 0;
|
||||
expect_word("release clears source suppression", 0xFFFFu,
|
||||
psx_keybinds_pad_word(keys, 1));
|
||||
{
|
||||
uint8_t host_keys[SDL_NUM_SCANCODES];
|
||||
psx_keybinds_host_keys(keys, host_keys);
|
||||
expect_word("released Escape is not Turbo", 0,
|
||||
(uint16_t)host_keymap_down(HOST_KEYMAP_TURBO, host_keys, 0));
|
||||
}
|
||||
keys[SDL_SCANCODE_ESCAPE] = 1;
|
||||
expect_word("fresh Escape press is native", 0x9FFFu,
|
||||
psx_keybinds_pad_word(keys, 1));
|
||||
{
|
||||
uint8_t host_keys[SDL_NUM_SCANCODES];
|
||||
psx_keybinds_host_keys(keys, host_keys);
|
||||
expect_word("fresh post-release native Escape activates Turbo", 1,
|
||||
(uint16_t)host_keymap_down(HOST_KEYMAP_TURBO, host_keys, 0));
|
||||
}
|
||||
psx_keybinds_suppress_control(1, PSXKB_MOUSE_SC(SDL_BUTTON_LEFT));
|
||||
expect_word("mouse suppression is P1 only", 0,
|
||||
(uint16_t)psx_keybinds_control_suppressed(2, PSXKB_MOUSE_SC(SDL_BUTTON_LEFT)));
|
||||
psx_keybinds_release_control(1, PSXKB_MOUSE_SC(SDL_BUTTON_LEFT));
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "keyboard pad chord: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "local_mouse_policy.h"
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { std::printf("FAIL line %d: %s\n", __LINE__, #x); return 1; } } while (0)
|
||||
namespace {
|
||||
constexpr uint64_t Ms=1000000;
|
||||
bool guest=true, capture_ok=true, active=false, held=false;
|
||||
unsigned captures=0, releases=0, notices=0, resets=0;
|
||||
unsigned hold_presses=0, hold_releases=0;
|
||||
psx::MouseControl suppressed=psx::MouseControl::None;
|
||||
bool capture(void*,bool on) { if(on) ++captures; else ++releases; return !on || capture_ok; }
|
||||
void suppress(void*,psx::MouseControl c) { suppressed=c; }
|
||||
void notice(void*,const char*) { ++notices; }
|
||||
int eligible(uint32_t buttons) { return guest && (buttons&9)==9; }
|
||||
void event(const PSXModMouseEvent* e) {
|
||||
if(e->type==PSX_MOD_MOUSE_RESET || e->type==PSX_MOD_MOUSE_ACQUIRED) { active=held=false; ++resets; }
|
||||
if(e->type==PSX_MOD_MOUSE_MOTION) active=true;
|
||||
if(e->type==PSX_MOD_MOUSE_HOLD_PRESS) { ++hold_presses;held=true;active=false; }
|
||||
if(e->type==PSX_MOD_MOUSE_HOLD_RELEASE) { ++hold_releases;active=held=false; }
|
||||
}
|
||||
void sample(uint64_t,PSXModMouseOutput* out) {
|
||||
out->override_right=1; out->rx=active?250:128; out->ry=active?64:128;
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
psx::LocalMousePolicy policy({nullptr,capture,suppress,notice});
|
||||
psx::LocalMouseHost host;
|
||||
host.live=host.focused=host.connected=host.analog=true;
|
||||
uint8_t x=99,y=77; policy.update(0,host); policy.sample(0,x,y); CHECK(x==99 && y==77);
|
||||
PSXModMousePolicy callbacks{sizeof(PSXModMousePolicy),PSX_MOD_MOUSE_HOLD_RIGHT,eligible,event,sample};
|
||||
CHECK(policy.install(&callbacks)); CHECK(!policy.install(&callbacks));
|
||||
policy.update(10*Ms,host);
|
||||
CHECK(policy.control(10*Ms,10*Ms,psx::MouseControl::Left,true,false));
|
||||
CHECK(policy.captured() && suppressed==psx::MouseControl::Left && !active);
|
||||
policy.sample(10*Ms,x,y); CHECK(x==128 && y==128 && notices==0);
|
||||
policy.motion(11*Ms,11*Ms,0.2,0.1); policy.sample(11*Ms,x,y); CHECK(x==250 && y==64);
|
||||
policy.sample(11*Ms,x,y); CHECK(x==250 && y==64);
|
||||
CHECK(policy.control(0,11*Ms,psx::MouseControl::Escape,true,false));
|
||||
CHECK(!policy.captured() && !active && suppressed==psx::MouseControl::Escape);
|
||||
CHECK(notices==0); // Normal activation, movement and emergency release are silent.
|
||||
CHECK(!policy.control(12*Ms,12*Ms,psx::MouseControl::Escape,true,false));
|
||||
uint64_t t=20*Ms;
|
||||
auto acquire=[&]() {
|
||||
t+=10*Ms;policy.update(t,host);
|
||||
policy.control(t,t,psx::MouseControl::Left,false,false);
|
||||
return policy.control(t,t,psx::MouseControl::Left,true,false);
|
||||
};
|
||||
CHECK(acquire());
|
||||
policy.control(t+Ms,t+Ms,psx::MouseControl::Right,true,false); CHECK(held && !active);
|
||||
policy.motion(t+2*Ms,t+2*Ms,30,0); CHECK(active);
|
||||
policy.control(UINT64_MAX,0,psx::MouseControl::Right,false,false); CHECK(!held && !active);
|
||||
policy.motion(t+3*Ms,t+3*Ms,30,0); CHECK(active);
|
||||
policy.control(t+4*Ms,t+4*Ms,psx::MouseControl::Right,true,false);
|
||||
policy.control(t+6*Ms,t+6*Ms,psx::MouseControl::Right,false,false); CHECK(!active);
|
||||
policy.motion(t+5*Ms,t+6*Ms,30,0); CHECK(!policy.captured() && !active);
|
||||
// Every lifecycle host gate neutralizes; restored eligibility needs a click.
|
||||
for(int gate=0;gate<7;++gate) {
|
||||
policy.reset();CHECK(acquire());policy.motion(t+Ms,t+Ms,30,0);
|
||||
auto off=host;
|
||||
if(gate==0)off.focused=false; if(gate==1)off.live=false;
|
||||
if(gate==2)off.connected=false;if(gate==3)off.analog=false;
|
||||
if(gate==4)off.native_right=true;if(gate==5)off.start=true;
|
||||
if(gate==6)guest=false;
|
||||
policy.update(t+2*Ms,off); CHECK(!policy.captured() && !active);
|
||||
guest=true;policy.update(t+3*Ms,host);
|
||||
CHECK(!policy.control(t+3*Ms,t+3*Ms,psx::MouseControl::Left,true,false));
|
||||
policy.motion(t+4*Ms,t+4*Ms,60,0); CHECK(!active && !policy.captured());
|
||||
x=1;y=255;policy.sample(t+4*Ms,x,y); CHECK(x==1 && y==255);
|
||||
}
|
||||
policy.reset();CHECK(acquire());
|
||||
policy.motion(t+2*Ms,t+Ms,10,0);CHECK(!policy.captured()); // future
|
||||
CHECK(acquire());policy.motion(t+Ms,t+Ms,10,0);
|
||||
policy.motion(t,t+Ms,10,0);CHECK(!policy.captured()); // backwards
|
||||
CHECK(acquire());policy.motion(t,t+251*Ms,10,0);CHECK(!policy.captured()); // stale
|
||||
CHECK(acquire());policy.motion(t,t,std::numeric_limits<double>::quiet_NaN(),0);CHECK(!policy.captured());
|
||||
CHECK(acquire());policy.motion(t+Ms,t+Ms,10,0);
|
||||
t+=300*Ms;policy.update(t,host);CHECK(!policy.captured() && !active);
|
||||
CHECK(!policy.control(t-Ms,t,psx::MouseControl::Left,true,false)); // no stall backlog rearm
|
||||
CHECK(acquire()); policy.control(t+Ms,t+Ms,psx::MouseControl::Right,true,false);
|
||||
policy.reset(); CHECK(acquire()); CHECK(!held);
|
||||
policy.motion(t+Ms,t+Ms,20,0); CHECK(active && !held); // held across reset cannot latch
|
||||
policy.control(t+2*Ms,t+2*Ms,psx::MouseControl::Right,false,false);
|
||||
policy.control(t+3*Ms,t+3*Ms,psx::MouseControl::Right,true,false);CHECK(held);
|
||||
policy.reset(); host.hold_conflict=true;CHECK(acquire());
|
||||
policy.control(t+Ms,t+Ms,psx::MouseControl::Right,false,false);
|
||||
policy.control(t+2*Ms,t+2*Ms,psx::MouseControl::Right,true,false);CHECK(!held);
|
||||
const unsigned before=notices;policy.update(t+3*Ms,host);CHECK(notices==before);
|
||||
|
||||
// Exact conflict -> acquire OTHER button -> hold down -> motion -> up.
|
||||
// A rejected physical edge must never send HOLD_RELEASE or kill a flick.
|
||||
policy.reset();
|
||||
policy.control(t+4*Ms,t+4*Ms,psx::MouseControl::Right,false,false);
|
||||
CHECK(acquire());
|
||||
const unsigned conflict_presses=hold_presses, conflict_releases=hold_releases;
|
||||
policy.control(t+Ms,t+Ms,psx::MouseControl::Right,true,false);
|
||||
policy.motion(t+2*Ms,t+2*Ms,30,0);CHECK(active && !held);
|
||||
policy.control(t+3*Ms,t+3*Ms,psx::MouseControl::Right,false,false);
|
||||
policy.sample(t+3*Ms,x,y);
|
||||
CHECK(policy.captured() && active && x==250 && y==64);
|
||||
CHECK(hold_presses==conflict_presses && hold_releases==conflict_releases);
|
||||
|
||||
// A physical hold already down before acquisition is also unaccepted.
|
||||
policy.reset();host.hold_conflict=false;
|
||||
auto unfocused=host;unfocused.focused=false;
|
||||
policy.update(t+4*Ms,unfocused);
|
||||
policy.control(t+4*Ms,t+4*Ms,psx::MouseControl::Right,true,false);
|
||||
CHECK(!policy.captured());
|
||||
CHECK(acquire());
|
||||
const unsigned blocked_presses=hold_presses, blocked_releases=hold_releases;
|
||||
policy.motion(t+Ms,t+Ms,30,0);CHECK(active && !held);
|
||||
policy.control(t+2*Ms,t+2*Ms,psx::MouseControl::Right,false,false);
|
||||
CHECK(policy.captured() && active && !held);
|
||||
CHECK(hold_presses==blocked_presses && hold_releases==blocked_releases);
|
||||
policy.control(t+3*Ms,t+3*Ms,psx::MouseControl::Right,true,false);
|
||||
CHECK(held && !active && hold_presses==blocked_presses+1);
|
||||
policy.motion(t+4*Ms,t+4*Ms,30,0);CHECK(active && held);
|
||||
policy.control(UINT64_MAX,0,psx::MouseControl::Right,false,false);
|
||||
CHECK(!active && !held && hold_releases==blocked_releases+1);
|
||||
|
||||
// If an accepted hold becomes conflicting, release it exactly once.
|
||||
policy.reset();CHECK(acquire());
|
||||
policy.control(t+Ms,t+Ms,psx::MouseControl::Right,true,false);
|
||||
policy.motion(t+2*Ms,t+2*Ms,30,0);CHECK(held && active);
|
||||
const unsigned accepted_releases=hold_releases;
|
||||
auto conflicting=host;conflicting.hold_conflict=true;
|
||||
policy.update(t+3*Ms,conflicting);
|
||||
CHECK(!held && !active && hold_releases==accepted_releases+1);
|
||||
policy.motion(t+4*Ms,t+4*Ms,30,0);CHECK(active && !held);
|
||||
policy.control(t+5*Ms,t+5*Ms,psx::MouseControl::Right,false,false);
|
||||
CHECK(active && hold_releases==accepted_releases+1);
|
||||
|
||||
// Activation belongs only to LEFT, and lasts only through its release.
|
||||
policy.reset();host.hold_conflict=false;
|
||||
policy.control(t+6*Ms,t+6*Ms,psx::MouseControl::Right,false,false);
|
||||
for (auto button : {psx::MouseControl::Middle, psx::MouseControl::Right,
|
||||
psx::MouseControl::X1, psx::MouseControl::X2}) {
|
||||
CHECK(!policy.control(t+7*Ms,t+7*Ms,button,true,false));
|
||||
CHECK(!policy.captured());
|
||||
policy.control(t+8*Ms,t+8*Ms,button,false,false);
|
||||
}
|
||||
const unsigned normal_notices=notices;
|
||||
CHECK(acquire());CHECK(!active);
|
||||
policy.motion(t+Ms,t+Ms,30,0);CHECK(active);
|
||||
CHECK(policy.control(UINT64_MAX,0,psx::MouseControl::Left,false,false));
|
||||
CHECK(!policy.captured() && !active && !held);
|
||||
x=99;y=77;policy.sample(t+Ms,x,y);CHECK(x==99 && y==77);
|
||||
CHECK(policy.control(t+2*Ms,t+2*Ms,psx::MouseControl::Left,true,false));
|
||||
policy.sample(t+2*Ms,x,y);CHECK(x==128 && y==128); // rapid re-press has no stale motion
|
||||
policy.motion(t+3*Ms,t+3*Ms,30,0);CHECK(active);
|
||||
policy.control(t+4*Ms,t+4*Ms,psx::MouseControl::Right,true,false);CHECK(held && !active);
|
||||
policy.motion(t+5*Ms,t+5*Ms,30,0);CHECK(held && active);
|
||||
CHECK(policy.control(t+6*Ms,t+6*Ms,psx::MouseControl::Left,false,false));
|
||||
CHECK(!policy.captured() && !active && !held); // RIGHT can never outlive LEFT
|
||||
CHECK(policy.control(t+7*Ms,t+7*Ms,psx::MouseControl::Left,true,false));
|
||||
CHECK(!held && !active); // RIGHT remains physically down, so requires a fresh edge
|
||||
policy.motion(t+8*Ms,t+8*Ms,30,0);CHECK(active && !held);
|
||||
policy.control(t+9*Ms,t+9*Ms,psx::MouseControl::Right,false,false);
|
||||
CHECK(active);CHECK(policy.control(t+10*Ms,t+10*Ms,psx::MouseControl::Left,false,false));
|
||||
CHECK(notices==normal_notices);
|
||||
|
||||
// A press while ineligible cannot activate later merely because eligibility returns.
|
||||
auto absent=host;absent.focused=false;
|
||||
policy.update(t+11*Ms,absent);
|
||||
CHECK(!policy.control(t+11*Ms,t+11*Ms,psx::MouseControl::Left,true,false));
|
||||
policy.update(t+12*Ms,host);
|
||||
CHECK(!policy.control(t+12*Ms,t+12*Ms,psx::MouseControl::Left,true,false));
|
||||
CHECK(!policy.captured());
|
||||
policy.control(t+13*Ms,t+13*Ms,psx::MouseControl::Left,false,false);
|
||||
CHECK(policy.control(t+14*Ms,t+14*Ms,psx::MouseControl::Left,true,false));
|
||||
CHECK(policy.captured() && !active);
|
||||
CHECK(policy.control(t+15*Ms,t+15*Ms,psx::MouseControl::Left,false,false));
|
||||
t+=20*Ms;
|
||||
|
||||
// SDL may drain several ordered edges after their physical timestamps.
|
||||
// A normal release must not reject a later re-press in that same drain.
|
||||
for (uint64_t delay : {10*Ms, 50*Ms, 200*Ms}) {
|
||||
psx::LocalMousePolicy batch({nullptr,capture,suppress,notice});
|
||||
CHECK(batch.install(&callbacks));
|
||||
const uint64_t base=1000*Ms+delay*10;
|
||||
batch.update(base+10*Ms,host);
|
||||
CHECK(batch.control(base+10*Ms,base+10*Ms,psx::MouseControl::Left,true,false));
|
||||
batch.motion(base+15*Ms,base+15*Ms,30,0);
|
||||
batch.control(base+16*Ms,base+16*Ms,psx::MouseControl::Right,true,false);
|
||||
batch.motion(base+17*Ms,base+17*Ms,30,0); CHECK(active && held);
|
||||
batch.update(base+20*Ms+delay,host);
|
||||
CHECK(batch.control(base+20*Ms,base+20*Ms+delay,psx::MouseControl::Left,false,false));
|
||||
CHECK(!batch.captured() && !active && !held);
|
||||
x=99;y=77;batch.sample(base+20*Ms+delay,x,y);CHECK(x==99 && y==77);
|
||||
batch.update(base+21*Ms+delay,host);
|
||||
CHECK(batch.control(base+25*Ms,base+21*Ms+delay,psx::MouseControl::Left,true,false));
|
||||
CHECK(batch.captured() && !active && !held);
|
||||
batch.sample(base+21*Ms+delay,x,y);CHECK(x==128 && y==128);
|
||||
batch.motion(base+26*Ms,base+22*Ms+delay,30,0);CHECK(active && !held);
|
||||
batch.control(base+27*Ms,base+23*Ms+delay,psx::MouseControl::Right,false,false);
|
||||
CHECK(active && !held); // A physically held RIGHT cannot latch or cancel this flick.
|
||||
CHECK(batch.control(base+28*Ms,base+24*Ms+delay,psx::MouseControl::Left,false,false));
|
||||
CHECK(!batch.captured() && !active && !held);
|
||||
batch.clear();
|
||||
}
|
||||
|
||||
// A release after an interruption cannot lower its processing-time barrier.
|
||||
for (int gate=0;gate<4;++gate) {
|
||||
psx::LocalMousePolicy batch({nullptr,capture,suppress,notice});
|
||||
CHECK(batch.install(&callbacks));
|
||||
const uint64_t base=5000*Ms+gate*1000*Ms;
|
||||
batch.update(base+10*Ms,host);
|
||||
CHECK(batch.control(base+10*Ms,base+10*Ms,psx::MouseControl::Left,true,false));
|
||||
batch.motion(base+15*Ms,base+15*Ms,30,0);
|
||||
auto off=host;const uint64_t barrier=base+(gate==3?300:30)*Ms;
|
||||
if(gate==0)off.focused=false;if(gate==1)guest=false;if(gate==2)off.native_right=true;
|
||||
batch.update(barrier,off);CHECK(!batch.captured() && !active);
|
||||
guest=true;batch.update(barrier+Ms,host);
|
||||
CHECK(batch.control(base+20*Ms,barrier+2*Ms,psx::MouseControl::Left,false,false));
|
||||
CHECK(!batch.control(base+25*Ms,barrier+3*Ms,psx::MouseControl::Left,true,false));
|
||||
CHECK(!batch.captured() && !active && !held);
|
||||
batch.control(barrier+4*Ms,barrier+4*Ms,psx::MouseControl::Left,false,false);
|
||||
batch.update(barrier+5*Ms,host);
|
||||
CHECK(batch.control(barrier+5*Ms,barrier+5*Ms,psx::MouseControl::Left,true,false));
|
||||
batch.sample(barrier+5*Ms,x,y);CHECK(x==128 && y==128);
|
||||
batch.motion(barrier+6*Ms,barrier+6*Ms,30,0);CHECK(active);
|
||||
batch.clear();
|
||||
}
|
||||
|
||||
// Malformed releases are still immediate, but cannot admit a queued press.
|
||||
for (uint64_t bad : {UINT64_MAX, uint64_t(14*Ms), uint64_t(0)}) {
|
||||
psx::LocalMousePolicy batch({nullptr,capture,suppress,notice});
|
||||
CHECK(batch.install(&callbacks));batch.update(10*Ms,host);
|
||||
CHECK(batch.control(10*Ms,10*Ms,psx::MouseControl::Left,true,false));
|
||||
batch.motion(15*Ms,15*Ms,30,0);batch.update(30*Ms,host);
|
||||
CHECK(batch.control(bad,30*Ms,psx::MouseControl::Left,false,false));
|
||||
CHECK(!batch.captured() && !active && !held);
|
||||
batch.update(31*Ms,host);
|
||||
CHECK(!batch.control(25*Ms,31*Ms,psx::MouseControl::Left,true,false));
|
||||
CHECK(!batch.captured());
|
||||
batch.control(32*Ms,32*Ms,psx::MouseControl::Left,false,false);
|
||||
batch.update(33*Ms,host);
|
||||
CHECK(batch.control(33*Ms,33*Ms,psx::MouseControl::Left,true,false));
|
||||
batch.sample(33*Ms,x,y);CHECK(x==128 && y==128);batch.clear();
|
||||
}
|
||||
|
||||
// LEFT released while unfocused is never delivered; reconcile on regain so
|
||||
// the next click activates. A still-held button is not a new activation.
|
||||
{
|
||||
psx::LocalMousePolicy lost({nullptr,capture,suppress,notice});
|
||||
CHECK(lost.install(&callbacks));
|
||||
const uint64_t base=9000*Ms;
|
||||
lost.update(base,host);
|
||||
CHECK(lost.control(base,base,psx::MouseControl::Left,true,false) && lost.captured());
|
||||
auto away=host;away.focused=false;
|
||||
lost.update(base+10*Ms,away);CHECK(!lost.captured());
|
||||
lost.update(base+20*Ms,host);
|
||||
lost.sync_left(base+20*Ms,true);
|
||||
CHECK(!lost.control(base+21*Ms,base+21*Ms,psx::MouseControl::Left,true,false));
|
||||
lost.sync_left(base+22*Ms,false);
|
||||
lost.update(base+23*Ms,host);
|
||||
CHECK(lost.control(base+23*Ms,base+23*Ms,psx::MouseControl::Left,true,false));
|
||||
CHECK(lost.captured());
|
||||
lost.sync_left(base+24*Ms,true);CHECK(lost.captured()); // down: no-op
|
||||
lost.clear();
|
||||
}
|
||||
|
||||
policy.reset();host.hold_conflict=false;capture_ok=false;CHECK(acquire());CHECK(!policy.captured());
|
||||
const unsigned failure_notices=notices;CHECK(acquire());CHECK(notices==failure_notices);
|
||||
policy.clear(); CHECK(!policy.installed() && !policy.captured());
|
||||
callbacks.struct_size=0;CHECK(!policy.install(&callbacks));
|
||||
std::printf("ordered local mouse policy safety: %u acquisitions, %u releases, %u resets\n",captures,releases,resets);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/* Exercise the real binding fold with a controlled physical mouse poll.
|
||||
* The adapter is tested separately; no production polling hook is added. */
|
||||
#include "psx_keybinds.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
static Uint32 mouse_buttons;
|
||||
static Uint32 test_mouse_state(void *x, void *y) { (void)x; (void)y; return mouse_buttons; }
|
||||
#define SDL_GetMouseState test_mouse_state
|
||||
#include "../src/psx_keybinds.c"
|
||||
#undef SDL_GetMouseState
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); return 1; } } while (0)
|
||||
int main(void) {
|
||||
uint8_t keys[SDL_NUM_SCANCODES] = {0};
|
||||
const SDL_Scancode left = PSXKB_MOUSE_SC(SDL_BUTTON_LEFT);
|
||||
psx_keybinds_reset_player(1); psx_keybinds_reset_player(2);
|
||||
/* Rebinding moves an assigned source; duplicate sources are a valid INI
|
||||
* state. Load that state without changing the real rebinding behavior. */
|
||||
#ifdef _WIN32
|
||||
CHECK(_mkdir("mouse_binding_suppression_fixture") == 0);
|
||||
#else
|
||||
CHECK(mkdir("mouse_binding_suppression_fixture", 0755) == 0);
|
||||
#endif
|
||||
FILE *config = fopen("mouse_binding_suppression_fixture/keybinds.ini", "wb");
|
||||
CHECK(config != NULL);
|
||||
fputs("[player1]\ncross = Mouse1\ncircle = S, Mouse1\n"
|
||||
"rs_right = Mouse1\nr1 = Mouse3\n[player2]\ncross = Mouse1\n", config);
|
||||
fclose(config);
|
||||
psx_keybinds_init("mouse_binding_suppression_fixture/fixture.exe");
|
||||
remove("mouse_binding_suppression_fixture/keybinds.ini");
|
||||
#ifdef _WIN32
|
||||
_rmdir("mouse_binding_suppression_fixture");
|
||||
#else
|
||||
rmdir("mouse_binding_suppression_fixture");
|
||||
#endif
|
||||
CHECK(psx_keybinds_get_button(1, PSX_KB_CROSS) == left);
|
||||
CHECK(psx_keybinds_get_button_alt(1, PSX_KB_CIRCLE) == left);
|
||||
CHECK(psx_keybinds_get_button(1, PSX_KB_RS_RIGHT) == left);
|
||||
mouse_buttons = SDL_BUTTON(SDL_BUTTON_LEFT);
|
||||
CHECK(psx_keybinds_pad_word(keys, 1) == 0x9FFF);
|
||||
psx_keybinds_suppress_control(1, left);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
uint8_t axes[4] = {128,128,128,128};
|
||||
CHECK(psx_keybinds_pad_word(keys, 1) == 0xFFFF);
|
||||
CHECK((0xBFFF & psx_keybinds_pad_word(keys, 1)) == 0xBFFF);
|
||||
psx_keybinds_sticks(keys, 1, axes);
|
||||
CHECK(axes[0] == 128 && axes[1] == 128 && axes[2] == 128 && axes[3] == 128);
|
||||
CHECK(psx_keybinds_pad_word(keys, 2) == 0xBFFF);
|
||||
}
|
||||
mouse_buttons = 0;
|
||||
CHECK(psx_keybinds_pad_word(keys, 1) == 0xFFFF);
|
||||
CHECK(!psx_keybinds_control_suppressed(1, left));
|
||||
mouse_buttons = SDL_BUTTON(SDL_BUTTON_LEFT);
|
||||
CHECK(psx_keybinds_pad_word(keys, 1) == 0x9FFF);
|
||||
uint8_t axes[4] = {128,128,128,128};
|
||||
psx_keybinds_sticks(keys, 1, axes); CHECK(axes[2] == 255);
|
||||
// Rapid owned re-press consumes LEFT again, while independent native
|
||||
// keyboard and RIGHT mouse bindings remain live before controller merging.
|
||||
psx_keybinds_suppress_control(1, left);
|
||||
keys[SDL_SCANCODE_S] = 1;
|
||||
mouse_buttons |= SDL_BUTTON(SDL_BUTTON_RIGHT);
|
||||
CHECK(psx_keybinds_pad_word(keys, 1) == 0xD7FF);
|
||||
CHECK((0xBFFF & psx_keybinds_pad_word(keys, 1)) == 0x97FF);
|
||||
// P2 retains both LEFT/Cross and its native S/Circle binding.
|
||||
CHECK(psx_keybinds_pad_word(keys, 2) == 0x9FFF);
|
||||
// Every poll starts with the new native report. The fold preserves axes
|
||||
// already supplied by a controller; it does not erase the previous query.
|
||||
axes[0] = axes[1] = axes[2] = axes[3] = 128;
|
||||
psx_keybinds_sticks(keys, 1, axes);CHECK(axes[2] == 128 && axes[3] == 128);
|
||||
axes[0] = 90; axes[1] = 160; axes[2] = 99; axes[3] = 77;
|
||||
psx_keybinds_sticks(keys, 1, axes);
|
||||
CHECK(axes[0] == 90 && axes[1] == 160 && axes[2] == 99 && axes[3] == 77);
|
||||
keys[SDL_SCANCODE_S] = 0;
|
||||
mouse_buttons = SDL_BUTTON(SDL_BUTTON_RIGHT);
|
||||
psx_keybinds_release_control(1, left);
|
||||
CHECK(psx_keybinds_pad_word(keys, 1) == 0xF7FF);
|
||||
CHECK(psx_keybinds_pad_word(keys, 2) == 0xFFFF);
|
||||
CHECK(!psx_keybinds_control_suppressed(1, left));
|
||||
mouse_buttons = 0;
|
||||
CHECK(psx_keybinds_pad_word(keys, 1) == 0xFFFF);
|
||||
puts("LEFT activation suppression survives polls/repress, preserves ordinary bindings/P2/controller, and releases");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/* Resolution order of external offline input (physical -> source -> mouse). */
|
||||
#include "pad_external_input.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CHECK(expr) do { if (!(expr)) { \
|
||||
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); return 1; } } while (0)
|
||||
|
||||
static struct {
|
||||
int guard, have_device, have_source, released;
|
||||
PsxNetPad local; PSXModControllerState src;
|
||||
int mouse_override; uint8_t mouse_rx, mouse_ry;
|
||||
char log[64]; int n;
|
||||
int capture_calls, source_calls, resolve_calls, release_calls;
|
||||
int mouse_resets, mouse_folds, last_guarded_cap, last_guarded_res;
|
||||
int fold_connected, fold_analog; uint16_t fold_buttons; uint8_t fold_rx, fold_ry;
|
||||
int fold_slot_calls[2];
|
||||
int src_analog_out;
|
||||
} T;
|
||||
|
||||
static void L(char c) { T.log[T.n++] = c; T.log[T.n] = 0; }
|
||||
static int h_guard(void *c) { (void)c; L('g'); return T.guard; }
|
||||
static int h_cap(void *c, int s, PsxNetPad *o, int g) {
|
||||
(void)c; (void)s; L('c'); T.capture_calls++; T.last_guarded_cap = g;
|
||||
if (!T.have_device) { memset(o, 0, sizeof *o); return 0; }
|
||||
*o = T.local; if (g) { o->buttons = 0xffff; o->rx = o->ry = 0x80; }
|
||||
return 1;
|
||||
}
|
||||
static int h_src(void *c, int s, PSXModControllerState *st, int *rel) {
|
||||
(void)c; (void)s; L('s'); T.source_calls++;
|
||||
if (!T.have_source) return 0;
|
||||
*st = T.src; *rel = T.released; return 1;
|
||||
}
|
||||
static void h_rel(void *c, int s, PsxNetPad *o) {
|
||||
(void)c; (void)s; L('R'); T.release_calls++;
|
||||
o->buttons = 0xffff; o->lx = o->ly = o->rx = o->ry = 128; o->analog = 1; o->connected = 1;
|
||||
}
|
||||
static void h_res(void *c, int s, const PSXModControllerState *st, int g, PsxNetPad *o) {
|
||||
(void)c; (void)s; L('r'); T.resolve_calls++; T.last_guarded_res = g;
|
||||
o->lx = (uint8_t)st->lx; o->ly = (uint8_t)st->ly;
|
||||
o->rx = (uint8_t)st->rx; o->ry = (uint8_t)st->ry;
|
||||
o->analog = (uint8_t)T.src_analog_out; o->connected = 1;
|
||||
if (!o->analog) o->lx = o->ly = o->rx = o->ry = 128;
|
||||
if (g) { o->buttons = 0xffff; o->lx = o->ly = o->rx = o->ry = 128; }
|
||||
}
|
||||
static void h_mreset(void *c) { (void)c; L('x'); T.mouse_resets++; }
|
||||
static void h_mfold(void *c, int con, int an, uint16_t b, uint8_t *rx, uint8_t *ry) {
|
||||
(void)c; L('m'); T.mouse_folds++;
|
||||
T.fold_connected = con; T.fold_analog = an; T.fold_buttons = b;
|
||||
T.fold_rx = *rx; T.fold_ry = *ry;
|
||||
if (T.mouse_override) { *rx = T.mouse_rx; *ry = T.mouse_ry; }
|
||||
}
|
||||
|
||||
static PadExtHooks hooks(void) {
|
||||
PadExtHooks h; memset(&h, 0, sizeof h);
|
||||
h.guard_active = h_guard; h.capture_local = h_cap; h.source_sample = h_src;
|
||||
h.source_release = h_rel; h.source_resolve = h_res;
|
||||
h.mouse_reset = h_mreset; h.mouse_fold = h_mfold;
|
||||
return h;
|
||||
}
|
||||
static void reset(void) {
|
||||
memset(&T, 0, sizeof T);
|
||||
T.have_device = 1; T.src_analog_out = 1;
|
||||
T.local.buttons = 0xfffe; /* SELECT held */
|
||||
T.local.lx = 10; T.local.ly = 20; T.local.rx = 30; T.local.ry = 40;
|
||||
T.local.analog = 1; T.local.connected = 1;
|
||||
T.src.struct_size = sizeof T.src; T.src.buttons = 0xfeff;
|
||||
T.src.lx = 200; T.src.ly = 5; T.src.rx = 128; T.src.ry = 128; T.src.analog = 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
PadExtHooks h = hooks();
|
||||
PsxNetPad o;
|
||||
|
||||
/* No source, no mouse effect: physical pad passes through bit-identical. */
|
||||
reset();
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1);
|
||||
CHECK(o.buttons == 0xfffe && o.lx == 10 && o.ly == 20 && o.rx == 30 &&
|
||||
o.ry == 40 && o.analog == 1 && o.connected == 1);
|
||||
CHECK(T.capture_calls == 1 && T.resolve_calls == 0 && T.release_calls == 0);
|
||||
CHECK(T.mouse_folds == 1 && T.fold_connected == 1 && T.fold_analog == 1 &&
|
||||
T.fold_buttons == 0xfffe && T.fold_rx == 30 && T.fold_ry == 40);
|
||||
CHECK(strcmp(T.log, "gscm") == 0);
|
||||
|
||||
/* Other ports never reach the mouse; no device: 0 and P1 resets it. */
|
||||
reset();
|
||||
CHECK(pad_ext_resolve(&h, 1, &o) == 1 && T.mouse_folds == 0 && T.mouse_resets == 0);
|
||||
reset(); T.have_device = 0;
|
||||
CHECK(pad_ext_resolve(&h, 1, &o) == 0 && T.mouse_resets == 0);
|
||||
reset(); T.have_device = 0;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 0 && T.mouse_resets == 1 && T.mouse_folds == 0);
|
||||
|
||||
/* Mouse only: overrides the right stick, left stick/buttons untouched. */
|
||||
reset(); T.local.rx = T.local.ry = 128; T.mouse_override = 1; T.mouse_rx = 250; T.mouse_ry = 7;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1);
|
||||
CHECK(o.rx == 250 && o.ry == 7 && o.lx == 10 && o.ly == 20 && o.buttons == 0xfffe);
|
||||
|
||||
/* Source only: buttons ANDed with physical, sticks/type from the source,
|
||||
* physical capture sampled exactly once, mouse offered the RESOLVED pad. */
|
||||
reset();
|
||||
T.have_source = 1; T.src.rx = 90; T.src.ry = 100;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1);
|
||||
CHECK(o.buttons == (0xfeff & 0xfffe) && o.lx == 200 && o.ly == 5 &&
|
||||
o.rx == 90 && o.ry == 100 && o.analog == 1 && o.connected == 1);
|
||||
CHECK(T.capture_calls == 1 && T.resolve_calls == 1);
|
||||
CHECK(T.fold_buttons == (0xfeff & 0xfffe) && T.fold_analog == 1 &&
|
||||
T.fold_rx == 90 && T.fold_ry == 100); /* not the physical 30/40 */
|
||||
CHECK(strcmp(T.log, "gscrm") == 0);
|
||||
|
||||
/* Source + mouse together: mouse overrides ONLY the right axes. */
|
||||
reset();
|
||||
T.have_source = 1; T.mouse_override = 1; T.mouse_rx = 33; T.mouse_ry = 44;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1);
|
||||
CHECK(o.rx == 33 && o.ry == 44 && o.lx == 200 && o.ly == 5 &&
|
||||
o.buttons == (0xfeff & 0xfffe) && T.mouse_resets == 0);
|
||||
|
||||
/* Source on a port with no physical device: source buttons as-is. */
|
||||
reset(); T.have_source = 1; T.have_device = 0;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1 && o.buttons == 0xfeff && o.connected == 1);
|
||||
CHECK(T.mouse_folds == 1 && T.mouse_resets == 0);
|
||||
|
||||
/* Digital source: sticks neutral, mouse sees analog=0. */
|
||||
reset(); T.have_source = 1; T.src_analog_out = 0;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1 && o.analog == 0 && o.lx == 128 &&
|
||||
T.fold_analog == 0);
|
||||
|
||||
/* Source release frame: neutral pad, mouse released, no physical capture. */
|
||||
reset(); T.have_source = 1; T.released = 1;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1 && o.buttons == 0xffff && o.lx == 128);
|
||||
CHECK(T.release_calls == 1 && T.capture_calls == 0 && T.resolve_calls == 0);
|
||||
CHECK(T.mouse_resets == 1 && T.mouse_folds == 0);
|
||||
reset(); T.have_source = 1; T.released = 1;
|
||||
CHECK(pad_ext_resolve(&h, 1, &o) == 1 && T.mouse_resets == 0);
|
||||
|
||||
/* Input guard: external input is neutral and the mouse is released; the
|
||||
* guard is forwarded to capture (physical) and resolve (source). */
|
||||
reset(); T.guard = 1;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1 && T.last_guarded_cap == 1);
|
||||
CHECK(o.buttons == 0xffff && o.rx == 128 && T.mouse_resets == 1 && T.mouse_folds == 0);
|
||||
reset(); T.guard = 1; T.have_source = 1;
|
||||
CHECK(pad_ext_resolve(&h, 0, &o) == 1 && T.last_guarded_res == 1);
|
||||
CHECK(o.buttons == 0xffff && o.lx == 128 && T.mouse_resets == 1 && T.mouse_folds == 0);
|
||||
|
||||
/* Gating predicate: live only when every blocking condition is clear. */
|
||||
{
|
||||
PadExtGate g; memset(&g, 0, sizeof g);
|
||||
CHECK(pad_ext_live(&g) == 1 && pad_ext_live(NULL) == 0);
|
||||
int *f[] = { &g.injected_input, &g.headless, &g.netplay_active,
|
||||
&g.netplay_resim, &g.selfcheck_locked, &g.selfcheck_resim,
|
||||
&g.render_pass, &g.savestate_menu_open, &g.rewind_open,
|
||||
&g.input_guard };
|
||||
for (unsigned i = 0; i < sizeof f / sizeof f[0]; i++) {
|
||||
*f[i] = 1; CHECK(pad_ext_live(&g) == 0); *f[i] = 0;
|
||||
}
|
||||
CHECK(pad_ext_live(&g) == 1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -30,11 +30,17 @@
|
||||
|
||||
#include "sio.h"
|
||||
#include "memcard.h"
|
||||
#include "psx_netplay.h"
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
/* ---- Stubs for sio.c's external dependencies ---- */
|
||||
uint32_t i_stat = 0;
|
||||
@@ -249,7 +255,11 @@ static void test_card_read_sector_0(void) {
|
||||
* only public entry. Let's symlink/copy our file as card1.mcd. */
|
||||
f = fopen("test_dir/card1.mcd", "wb");
|
||||
if (!f) {
|
||||
system("mkdir test_dir 2>/dev/null || mkdir -p test_dir");
|
||||
#ifdef _WIN32
|
||||
(void)_mkdir("test_dir");
|
||||
#else
|
||||
(void)mkdir("test_dir", 0755);
|
||||
#endif
|
||||
f = fopen("test_dir/card1.mcd", "wb");
|
||||
}
|
||||
assert(f);
|
||||
@@ -414,8 +424,57 @@ static void test_cycle_paced_exact_boundaries(void) {
|
||||
EXPECT_EQ("paced.tick.irq", irq_sio0, i_stat & irq_sio0);
|
||||
}
|
||||
|
||||
/* Local input publishes RX/RY through the ordinary pad state/stick/type APIs.
|
||||
* Pin the actual wire order, P2 independence and idle-boundary type change. */
|
||||
static void test_local_pad_report_delivery(void) {
|
||||
PsxNetPad p1 = {0xA55A, 0x11, 0xE7, 0xF1, 0x17, 1, 1};
|
||||
PsxNetPad p2 = {0x3C81, 0x33, 0x55, 0x99, 0xAA, 1, 1};
|
||||
EXPECT_EQ("pad.blob.size", 8, sizeof(PsxNetPad));
|
||||
sio_init();
|
||||
sio_connect_pad(0); sio_connect_pad(1);
|
||||
sio_set_pad_analog(0, 1, 128, 128, 128, 128);
|
||||
sio_set_pad_analog(1, 1, 128, 128, 128, 128);
|
||||
sio_set_pad_state_slot(0, p1.buttons);
|
||||
sio_set_pad_sticks(0, p1.lx, p1.ly, p1.rx, p1.ry);
|
||||
sio_request_pad_type(0, p1.analog);
|
||||
sio_set_pad_state_slot(1, p2.buttons);
|
||||
sio_set_pad_sticks(1, p2.lx, p2.ly, p2.rx, p2.ry);
|
||||
sio_request_pad_type(1, p2.analog);
|
||||
card_deselect(0);
|
||||
EXPECT_EQ("pad.p1.prefix", 0xFF, card_xchg(0x01, 0));
|
||||
EXPECT_EQ("pad.p1.id", 0x73, card_xchg(0x42, 0));
|
||||
/* A frame's requested type cannot truncate an already-started report. */
|
||||
sio_request_pad_type(0, 0);
|
||||
EXPECT_EQ("pad.p1.marker", 0x5A, card_xchg(0, 0));
|
||||
EXPECT_EQ("pad.p1.buttons.low", 0x5A, card_xchg(0, 0));
|
||||
EXPECT_EQ("pad.p1.buttons.high", 0xA5, card_xchg(0, 0));
|
||||
EXPECT_EQ("pad.p1.rx", 0xF1, card_xchg(0, 0));
|
||||
EXPECT_EQ("pad.p1.ry", 0x17, card_xchg(0, 0));
|
||||
EXPECT_EQ("pad.p1.lx", 0x11, card_xchg(0, 0));
|
||||
EXPECT_EQ("pad.p1.ly", 0xE7, card_xchg(0, 0));
|
||||
card_deselect(0);
|
||||
EXPECT_EQ("pad.digital.prefix", 0xFF, card_xchg(0x01, 0));
|
||||
EXPECT_EQ("pad.digital.id", 0x41, card_xchg(0x42, 0));
|
||||
EXPECT_EQ("pad.digital.marker", 0x5A, card_xchg(0, 0));
|
||||
EXPECT_EQ("pad.digital.buttons.low", 0x5A, card_xchg(0, 0));
|
||||
EXPECT_EQ("pad.digital.buttons.high", 0xA5, card_xchg(0, 0));
|
||||
card_deselect(1);
|
||||
EXPECT_EQ("pad.p2.prefix", 0xFF, card_xchg(0x01, 1));
|
||||
EXPECT_EQ("pad.p2.id", 0x73, card_xchg(0x42, 1));
|
||||
EXPECT_EQ("pad.p2.marker", 0x5A, card_xchg(0, 1));
|
||||
EXPECT_EQ("pad.p2.buttons.low", 0x81, card_xchg(0, 1));
|
||||
EXPECT_EQ("pad.p2.buttons.high", 0x3C, card_xchg(0, 1));
|
||||
EXPECT_EQ("pad.p2.rx", 0x99, card_xchg(0, 1));
|
||||
EXPECT_EQ("pad.p2.ry", 0xAA, card_xchg(0, 1));
|
||||
EXPECT_EQ("pad.p2.lx", 0x33, card_xchg(0, 1));
|
||||
EXPECT_EQ("pad.p2.ly", 0x55, card_xchg(0, 1));
|
||||
card_deselect(1);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
fprintf(stderr, "=== sio card protocol tests ===\n");
|
||||
test_local_pad_report_delivery();
|
||||
fprintf(stderr, "test_local_pad_report_delivery: %d/%d ok\n", g_checks-g_failures, g_checks);
|
||||
|
||||
if (argc == 2 && strcmp(argv[1], "exact-boundaries") == 0) {
|
||||
test_cycle_paced_exact_boundaries();
|
||||
|
||||
Reference in New Issue
Block a user