mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
Support optional held mouse input policies for local P1
(cherry picked from commit 00ee1f40b6)
This commit is contained in:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -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