Support optional held mouse input policies for local P1

(cherry picked from commit 00ee1f40b6)
This commit is contained in:
Beman
2026-10-04 23:57:16 +10:00
committed by Matthew Stanley
parent 9aacc1f01e
commit cd9cb8c48b
15 changed files with 1203 additions and 124 deletions
+86
View File
@@ -0,0 +1,86 @@
# Optional local mouse policy
The SDL-free `PSXModMousePolicy` interface provides one trusted local P1 policy.
Register it from an opt-in activation plugin. 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
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.
`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. Final game-pin
reachability/CI and physical interruption checks are pending; this is not a
public-quality or merge-readiness claim.
+12 -1
View File
@@ -23,6 +23,10 @@ psxrecomp_add_runtime_target(psx-runtime
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
if(BUILD_TESTING)
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
@@ -345,7 +349,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
@@ -354,6 +358,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.
+39
View File
@@ -586,6 +586,45 @@ int psx_mod_set_controller_presentation_policy(
uint32_t initial_mode,
int config_capable);
/* Optional local P1 mouse policy. The runtime delivers ordered events on the
* SDL owner thread, owns relative capture and folds the resulting right-stick
* bytes after native input/presentation, before normal SIO delivery. No SDL
* type, guest address, gesture, or game setting belongs in this interface.
* This first version is inert during netplay, injected input and replay.
* Exactly one trusted activation plugin may register per session. */
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
+17
View File
@@ -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
+2
View File
@@ -340,6 +340,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
+161
View File
@@ -0,0 +1,161 @@
#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.
const bool owned = activation_owned_;
activation_owned_ = false;
if (captured_ || owned) reset();
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::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
+47
View File
@@ -0,0 +1,47 @@
#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);
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
+206
View File
@@ -0,0 +1,206 @@
#include "local_mouse_sdl.h"
#include "local_mouse_policy.h"
#include "psx_keybinds.h"
#include "host_osd.h"
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_);
#if defined(PSX_SDL3)
return (flags & (SDL_WINDOW_HIDDEN | SDL_WINDOW_MINIMIZED)) == 0;
#else
return (flags & (SDL_WINDOW_HIDDEN | SDL_WINDOW_MINIMIZED)) == 0;
#endif
}
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) {
return state().install(policy) ? 1 : 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) {
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 catch a lost release only while focused and captured. Neither path
// manufactures a down or unblocks a held activation across focus loss.
#if defined(_WIN32)
const bool left_released = !(SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT));
#else
const bool left_released = host_.focused && state().captured() &&
!(SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT));
#endif
if (left_released)
state().control(0, now_ns(), psx::MouseControl::Left, false, false);
// 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) {
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);
}
+11
View File
@@ -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);
+171 -114
View File
@@ -29,6 +29,7 @@
#include "psx_rewind.h"
#include "psx_savestate_menu.h"
#include "host_osd.h"
#include "local_mouse_sdl.h"
#include "host_keymap.h"
#include "png_write.h" /* png_write_rgb — present_shot readback */
#include "overlay_capture.h"
@@ -1042,6 +1043,7 @@ extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok)
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();
@@ -1539,6 +1541,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();
g_mod_texfilter = -1;
gr_set_texture_filter(g_video_texfilter);
PSXModSessionScalars live;
@@ -3646,6 +3649,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();
@@ -4999,6 +5003,7 @@ static int effective_player_mode_for_sio(const PlayerInput& p, int sio_slot) {
* 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];
@@ -5512,6 +5517,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;
@@ -5637,7 +5643,10 @@ static int capture_pad_slot(int s, PsxNetPad* out) {
/* Opt-in dev merge: P1 is driven by the keyboard AND every connected
* controller (PSX_DEV_INPUT=1). Default is strict per-slot routing. */
const bool dev_here = (dev_any_input_enabled() && s == 0);
if (p.kind == 0 && !dev_here) return 0; /* no device in this port */
if (p.kind == 0 && !dev_here) {
if (s == 0) psx_local_mouse_reset();
return 0; /* no device in this port */
}
/* Resolve the pad type this frame FIRST — the effective analog/digital
* state gates how the left stick is read for BOTH the button word and the
@@ -5663,6 +5672,7 @@ static int capture_pad_slot(int s, PsxNetPad* out) {
const int eff_analog =
effective_mode == PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
if (savestate_input_guard_active()) {
if (s == 0) psx_local_mouse_reset();
out->buttons = 0xFFFFu;
out->lx = out->ly = out->rx = out->ry = 0x80u;
out->analog = eff_analog ? 1u : 0u;
@@ -5709,6 +5719,7 @@ static int capture_pad_slot(int s, PsxNetPad* out) {
st[0] = st[1] = st[2] = st[3] = 0x80;
}
if (s == 0) psx_local_mouse_pad(true, eff_analog != 0, btn, st[2], st[3]);
out->buttons = btn;
out->lx = st[0]; out->ly = st[1]; out->rx = st[2]; out->ry = st[3];
out->analog = eff_analog ? 1u : 0u;
@@ -6235,6 +6246,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;
}
@@ -6715,6 +6727,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) {
@@ -6878,8 +6891,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;
}
@@ -6984,10 +6999,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();
@@ -7028,10 +7046,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();
@@ -7084,6 +7105,138 @@ struct NetplayVblankEpilogue {
};
/* Called from gpu_vblank_tick() at each simulated vblank. */
// 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_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) {
bool injected = override_word >= 0;
#ifndef PSX_NO_DEBUG_TOOLS
uint8_t axes[4];
injected = injected || debug_server_get_axis_override(axes) != 0;
#endif
return !injected && !g_headless &&
!psx_netplay_active() && !psx_netplay_is_resimulating() &&
!psx_selfcheck_input_locked() && !psx_selfcheck_resim_active() &&
!g_psx_render_pass_active && !savestate_menu_open &&
!psx_rewind_is_open() && !savestate_input_guard_active();
}
static NetplayVblankEpilogue sdl_vblank_present_body(void) {
NetplayVblankEpilogue ep{};
/* Guest quantum for this vblank is complete. Drop top-level-resume armed
@@ -7118,6 +7271,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;
@@ -7240,116 +7394,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_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();
@@ -7566,8 +7612,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();
@@ -7697,7 +7745,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();
}
+35 -8
View File
@@ -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) {
+85
View File
@@ -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;
+182
View File
@@ -0,0 +1,182 @@
#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;
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;
}
+60 -1
View File
@@ -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();