Preserve ordered mouse re-press after delayed release

(cherry picked from commit 50ec9191c2)
This commit is contained in:
Beman
2026-10-05 01:07:18 +10:00
committed by Matthew Stanley
parent b5566c7b25
commit 93371b1782
3 changed files with 86 additions and 2 deletions
+10 -2
View File
@@ -45,6 +45,11 @@ routing/removal, invalid data, capture failure and >250 ms host stalls release
capture and reset the policy. Stale, backwards and future motion cannot
extend a pulse. Releases remain immediate even with anomalous timestamps;
their watermark cannot admit old motion after a cleared held target. A
valid ordered LEFT release while capture remains eligible uses its actual
event timestamp as the acquisition boundary. A later physical press in the
same delayed SDL drain can therefore acquire a neutral new session. Lifecycle,
stall and malformed-release resets retain the processing-time barrier; a release
after such a reset cannot lower it to admit queued input. A
stall's backlog cannot acquire capture again. Eligibility recovery needs a
release/repress of LEFT and fresh motion. Reset retains physical LEFT down-state,
so holding it across an interruption cannot recapture. Debug input injection, headless, netplay, replay/resimulation and
@@ -81,6 +86,9 @@ 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
existing mod APIs and needs separate compatibility validation. The linked game
draft pins this runtime draft's reachable head through its configured framework
upstream; final-pin checkout, build and local test validation are recorded with
the drafts. Hosted CI status and physical interruption checks are reported
separately; this is not a
public-quality or merge-readiness claim.
+9
View File
@@ -86,9 +86,18 @@ bool LocalMousePolicy::control(uint64_t t, uint64_t now, MouseControl control,
if (!down) {
// Release is unconditional: a future/backwards event time cannot
// leave capture or the reducer's separate direction hold active.
// A valid ordered release while still eligible owns its event
// boundary, rather than the later time at which SDL drains it.
// After an interruption or malformed release, retain reset()'s
// processing-time barrier so queued input cannot rearm capture.
const bool ordered_release = captured_ && allowed() &&
t <= now && now - t <= StallNs && t >= release_clock_ &&
(!event_valid_ || t >= event_clock_) &&
(!clock_valid_ || (now >= clock_ && now - clock_ <= StallNs));
const bool owned = activation_owned_;
activation_owned_ = false;
if (captured_ || owned) reset();
if (ordered_release) release_clock_ = t;
return owned;
}
// reset() retains physical down-state. A held button after a menu,
+67
View File
@@ -173,6 +173,73 @@ int main() {
CHECK(policy.control(t+15*Ms,t+15*Ms,psx::MouseControl::Left,false,false));
t+=20*Ms;
// SDL may drain several ordered edges after their physical timestamps.
// A normal release must not reject a later re-press in that same drain.
for (uint64_t delay : {10*Ms, 50*Ms, 200*Ms}) {
psx::LocalMousePolicy batch({nullptr,capture,suppress,notice});
CHECK(batch.install(&callbacks));
const uint64_t base=1000*Ms+delay*10;
batch.update(base+10*Ms,host);
CHECK(batch.control(base+10*Ms,base+10*Ms,psx::MouseControl::Left,true,false));
batch.motion(base+15*Ms,base+15*Ms,30,0);
batch.control(base+16*Ms,base+16*Ms,psx::MouseControl::Right,true,false);
batch.motion(base+17*Ms,base+17*Ms,30,0); CHECK(active && held);
batch.update(base+20*Ms+delay,host);
CHECK(batch.control(base+20*Ms,base+20*Ms+delay,psx::MouseControl::Left,false,false));
CHECK(!batch.captured() && !active && !held);
x=99;y=77;batch.sample(base+20*Ms+delay,x,y);CHECK(x==99 && y==77);
batch.update(base+21*Ms+delay,host);
CHECK(batch.control(base+25*Ms,base+21*Ms+delay,psx::MouseControl::Left,true,false));
CHECK(batch.captured() && !active && !held);
batch.sample(base+21*Ms+delay,x,y);CHECK(x==128 && y==128);
batch.motion(base+26*Ms,base+22*Ms+delay,30,0);CHECK(active && !held);
batch.control(base+27*Ms,base+23*Ms+delay,psx::MouseControl::Right,false,false);
CHECK(active && !held); // A physically held RIGHT cannot latch or cancel this flick.
CHECK(batch.control(base+28*Ms,base+24*Ms+delay,psx::MouseControl::Left,false,false));
CHECK(!batch.captured() && !active && !held);
batch.clear();
}
// A release after an interruption cannot lower its processing-time barrier.
for (int gate=0;gate<4;++gate) {
psx::LocalMousePolicy batch({nullptr,capture,suppress,notice});
CHECK(batch.install(&callbacks));
const uint64_t base=5000*Ms+gate*1000*Ms;
batch.update(base+10*Ms,host);
CHECK(batch.control(base+10*Ms,base+10*Ms,psx::MouseControl::Left,true,false));
batch.motion(base+15*Ms,base+15*Ms,30,0);
auto off=host;const uint64_t barrier=base+(gate==3?300:30)*Ms;
if(gate==0)off.focused=false;if(gate==1)guest=false;if(gate==2)off.native_right=true;
batch.update(barrier,off);CHECK(!batch.captured() && !active);
guest=true;batch.update(barrier+Ms,host);
CHECK(batch.control(base+20*Ms,barrier+2*Ms,psx::MouseControl::Left,false,false));
CHECK(!batch.control(base+25*Ms,barrier+3*Ms,psx::MouseControl::Left,true,false));
CHECK(!batch.captured() && !active && !held);
batch.control(barrier+4*Ms,barrier+4*Ms,psx::MouseControl::Left,false,false);
batch.update(barrier+5*Ms,host);
CHECK(batch.control(barrier+5*Ms,barrier+5*Ms,psx::MouseControl::Left,true,false));
batch.sample(barrier+5*Ms,x,y);CHECK(x==128 && y==128);
batch.motion(barrier+6*Ms,barrier+6*Ms,30,0);CHECK(active);
batch.clear();
}
// Malformed releases are still immediate, but cannot admit a queued press.
for (uint64_t bad : {UINT64_MAX, uint64_t(14*Ms), uint64_t(0)}) {
psx::LocalMousePolicy batch({nullptr,capture,suppress,notice});
CHECK(batch.install(&callbacks));batch.update(10*Ms,host);
CHECK(batch.control(10*Ms,10*Ms,psx::MouseControl::Left,true,false));
batch.motion(15*Ms,15*Ms,30,0);batch.update(30*Ms,host);
CHECK(batch.control(bad,30*Ms,psx::MouseControl::Left,false,false));
CHECK(!batch.captured() && !active && !held);
batch.update(31*Ms,host);
CHECK(!batch.control(25*Ms,31*Ms,psx::MouseControl::Left,true,false));
CHECK(!batch.captured());
batch.control(32*Ms,32*Ms,psx::MouseControl::Left,false,false);
batch.update(33*Ms,host);
CHECK(batch.control(33*Ms,33*Ms,psx::MouseControl::Left,true,false));
batch.sample(33*Ms,x,y);CHECK(x==128 && y==128);batch.clear();
}
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());