Repository navigation
fix(gamescope): direct-touch drags work again in games that confine a hidden cursor #2220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4cd9415
f3dea94
27d0c0e
de57e44
60be5ba
79240a8
343acf1
316355a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp | ||
| --- a/src/steamcompmgr.cpp | ||
| +++ b/src/steamcompmgr.cpp | ||
| @@ -3165,6 +3165,128 @@ | ||
| wlserver_unlock(); | ||
| } | ||
|
|
||
| +struct PointerHold | ||
| +{ | ||
| + int nAwayFrames = 0; | ||
| + int nFreeFrames = 0; | ||
| + uint64_t ulFirstAway = 0; | ||
| + bool bHeld = false; | ||
| +}; | ||
| + | ||
| +static bool pointer_hold_step( PointerHold &state, bool bConstrained, double flSeatDX, double flSeatDY, double flPointerDX, double flPointerDY, uint64_t ulNow ) | ||
| +{ | ||
| + if ( !bConstrained ) | ||
| + { | ||
| + state = PointerHold{}; | ||
| + return false; | ||
| + } | ||
| + | ||
| + const double flSeat = std::max( std::abs( flSeatDX ), std::abs( flSeatDY ) ); | ||
| + if ( flSeat < 4.0 ) | ||
| + return state.bHeld; | ||
| + | ||
| + const double flMiss = std::max( std::abs( flSeatDX - flPointerDX ), std::abs( flSeatDY - flPointerDY ) ); | ||
| + if ( flMiss > flSeat * 0.75 ) | ||
| + { | ||
| + if ( state.nAwayFrames == 0 || ulNow - state.ulFirstAway > 500'000'000ul ) | ||
| + { | ||
| + state.nAwayFrames = 0; | ||
| + state.ulFirstAway = ulNow; | ||
| + } | ||
| + state.nFreeFrames = 0; | ||
| + if ( ++state.nAwayFrames >= 3 ) | ||
| + state.bHeld = true; | ||
| + } | ||
| + else if ( state.bHeld && ++state.nFreeFrames >= 30 ) | ||
| + { | ||
| + state = PointerHold{}; | ||
| + } | ||
| + | ||
| + return state.bHeld; | ||
| +} | ||
| + | ||
| +static bool s_bPointerHeld = false; | ||
| + | ||
| +static void update_pointer_hold( global_focus_t *pFocus, bool bConstrained, bool bHinted ) | ||
| +{ | ||
| + static PointerHold s_Hold; | ||
| + static bool s_bTracking = false; | ||
| + static double s_flSeatX = 0.0; | ||
| + static double s_flSeatY = 0.0; | ||
| + static int s_nRootX = 0; | ||
| + static int s_nRootY = 0; | ||
| + static bool s_bHinted = false; | ||
| + static const steamcompmgr_win_t *s_pWindow = nullptr; | ||
| + static const struct wlr_pointer_constraint_v1 *s_pConstraint = nullptr; | ||
| + | ||
| + wlserver_lock(); | ||
| + const double flX = wlserver.mouse_surface_cursorx; | ||
| + const double flY = wlserver.mouse_surface_cursory; | ||
| + const double flSentX = wlserver.flSentDX; | ||
| + const double flSentY = wlserver.flSentDY; | ||
| + wlserver.flSentDX = 0.0; | ||
| + wlserver.flSentDY = 0.0; | ||
| + wlserver_unlock(); | ||
| + | ||
| + if ( !bConstrained || !pFocus->cursor ) | ||
| + { | ||
| + s_bTracking = false; | ||
| + s_bHinted = false; | ||
| + s_bPointerHeld = pointer_hold_step( s_Hold, false, 0.0, 0.0, 0.0, 0.0, 0 ); | ||
| + return; | ||
| + } | ||
| + | ||
| + const steamcompmgr_win_t *pWindow = pFocus->inputFocusWindow; | ||
| + const struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint(); | ||
| + if ( pWindow != s_pWindow || pConstraint != s_pConstraint ) | ||
| + { | ||
| + s_bTracking = false; | ||
| + s_bHinted = false; | ||
| + } | ||
| + s_pWindow = pWindow; | ||
| + s_pConstraint = pConstraint; | ||
| + | ||
| + if ( s_bTracking && bHinted && !s_bHinted ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| + { | ||
| + s_Hold = PointerHold{}; | ||
| + s_Hold.bHeld = true; | ||
| + s_bPointerHeld = true; | ||
| + } | ||
| + s_bHinted = bHinted; | ||
| + | ||
| + double flDX = flX - s_flSeatX; | ||
| + double flDY = flY - s_flSeatY; | ||
| + if ( flDX == 0.0 && flDY == 0.0 ) | ||
| + { | ||
| + flDX = flSentX; | ||
| + flDY = flSentY; | ||
| + } | ||
| + | ||
| + if ( s_bTracking && flDX == 0.0 && flDY == 0.0 ) | ||
| + return; | ||
| + | ||
| + int nRootX = 0; | ||
| + int nRootY = 0; | ||
| + pFocus->cursor->QueryRootPosition( &nRootX, &nRootY ); | ||
| + | ||
| + if ( s_bTracking ) | ||
| + { | ||
| + s_bPointerHeld = pointer_hold_step( s_Hold, true, flDX, flDY, nRootX - s_nRootX, nRootY - s_nRootY, get_time_in_nanos() ); | ||
| + } | ||
| + else | ||
| + { | ||
| + s_Hold = PointerHold{}; | ||
| + s_Hold.bHeld = bHinted; | ||
| + s_bPointerHeld = bHinted; | ||
| + } | ||
| + | ||
| + s_bTracking = true; | ||
| + s_flSeatX = flX; | ||
| + s_flSeatY = flY; | ||
| + s_nRootX = nRootX; | ||
| + s_nRootY = nRootY; | ||
| +} | ||
| + | ||
| bool ShouldDrawCursor() | ||
| { | ||
| if ( relative_mouse_forced() ) | ||
| @@ -11029,9 +11151,15 @@ | ||
| nRelativeMouse != 2 && !bExcludedAppId && !bHasPointerConstraint && !window_is_steam( pPaintFocus->inputFocusWindow ) ); | ||
| } | ||
|
|
||
| + if ( vblank ) | ||
| + update_pointer_hold( pPaintFocus, bHasPointerConstraint && bImageEmpty && nRelativeMouse != 2 && !bExcludedAppId, | ||
| + pPaintFocus->cursor && pPaintFocus->cursor->IsConstrained() ); | ||
| + | ||
| + const bool bGameHoldsPointer = bHasPointerConstraint && s_bPointerHeld; | ||
| + | ||
| const bool bRelativeMouseMode = nRelativeMouse == 2 | ||
| ? false | ||
| - : ( bForcedRelativeMouse || s_bFollowClientPointer || ( bImageEmpty && bHasPointerConstraint && !bExcludedAppId ) ); | ||
| + : ( bForcedRelativeMouse || s_bFollowClientPointer || ( bImageEmpty && bGameHoldsPointer && !bExcludedAppId ) ); | ||
|
|
||
| static bool s_bRelativeMouseMode = false; | ||
| if ( s_bRelativeMouseMode && !bRelativeMouseMode && pPaintFocus->cursor && | ||
| diff --git a/src/wlserver.cpp b/src/wlserver.cpp | ||
| --- a/src/wlserver.cpp | ||
| +++ b/src/wlserver.cpp | ||
| @@ -346,6 +346,8 @@ | ||
| { | ||
| assert( wlserver_is_lock_held() ); | ||
|
|
||
| + wlserver.flSentDX += unaccel_dx; | ||
| + wlserver.flSentDY += unaccel_dy; | ||
| wlr_relative_pointer_manager_v1_send_relative_motion( wlserver.relative_pointer_manager, wlserver.wlr.seat, 0, unaccel_dx, unaccel_dy, unaccel_dx, unaccel_dy ); | ||
| } | ||
|
|
||
| diff --git a/src/wlserver.hpp b/src/wlserver.hpp | ||
| --- a/src/wlserver.hpp | ||
| +++ b/src/wlserver.hpp | ||
| @@ -171,6 +171,8 @@ | ||
| bool bFollowClientPointer = false; | ||
| double flFollowDX = 0.0; | ||
| double flFollowDY = 0.0; | ||
| + double flSentDX = 0.0; | ||
| + double flSentDY = 0.0; | ||
|
|
||
| bool button_held[ WLSERVER_BUTTON_COUNT ]; | ||
| std::set <uint32_t> touch_down_ids; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| from pathlib import Path | ||
| import shutil | ||
| import subprocess | ||
| import tempfile | ||
| import unittest | ||
|
|
||
|
|
||
| ROOT = Path(__file__).resolve().parents[2] | ||
| PATCH = ROOT / "tools/gamescope/patches/0133-constraint-relative-when-held.patch" | ||
|
|
||
|
|
||
| def added_source(): | ||
| return "\n".join(line[1:] for line in PATCH.read_text().splitlines() if line.startswith("+") and not line.startswith("+++")) | ||
|
|
||
|
|
||
| def block(text, start): | ||
| begin = text.index(start) | ||
| return text[begin:text.index("\n}", begin) + 3] | ||
|
|
||
|
|
||
| def rules(): | ||
| text = added_source() | ||
| return "\n".join([ | ||
| block(text, "struct PointerHold"), | ||
| block(text, "static bool pointer_hold_step"), | ||
| ]) | ||
|
|
||
|
|
||
| PREAMBLE = r''' | ||
| #include <algorithm> | ||
| #include <cassert> | ||
| #include <cmath> | ||
| #include <cstdint> | ||
| #include <cstdio> | ||
| ''' | ||
|
|
||
| MAIN = r''' | ||
| static const uint64_t FRAME = 16'666'667ul; | ||
|
|
||
| static bool run(PointerHold &state, int frames, double seatDX, double seatDY, double pointerDX, double pointerDY, uint64_t &now, bool constrained = true) { | ||
| bool held = state.bHeld; | ||
| for (int i = 0; i < frames; i++) { | ||
| now += FRAME; | ||
| held = pointer_hold_step(state, constrained, seatDX, seatDY, pointerDX, pointerDY, now); | ||
| } | ||
| return held; | ||
| } | ||
|
|
||
| int main() { | ||
| uint64_t now = 1'000'000'000ul; | ||
|
|
||
| PointerHold cursorGame; | ||
| assert(!run(cursorGame, 600, 12.0, -9.0, 12.0, -9.0, now)); | ||
| assert(!run(cursorGame, 600, 40.0, 3.0, 39.0, 3.0, now)); | ||
| assert(!run(cursorGame, 1, 30.0, 0.0, 0.0, 0.0, now)); | ||
| assert(!run(cursorGame, 1, 30.0, 0.0, 30.0, 0.0, now)); | ||
| assert(!run(cursorGame, 1, 30.0, 0.0, 0.0, 0.0, now)); | ||
| assert(!run(cursorGame, 120, 2.0, 1.0, 0.0, 0.0, now)); | ||
|
|
||
| PointerHold offset; | ||
| assert(!run(offset, 600, 25.0, 25.0, 25.0, 25.0, now)); | ||
|
|
||
| PointerHold recentring; | ||
| assert(!run(recentring, 2, -40.0, 12.0, 0.0, 0.0, now)); | ||
| assert(run(recentring, 1, -40.0, 12.0, 1.0, 0.0, now)); | ||
| assert(run(recentring, 600, 0.0, 0.0, 0.0, 0.0, now)); | ||
| assert(run(recentring, 29, 10.0, 4.0, 10.0, 4.0, now)); | ||
| assert(run(recentring, 1, 20.0, 0.0, 0.0, 0.0, now)); | ||
| assert(run(recentring, 29, 10.0, 4.0, 10.0, 4.0, now)); | ||
| assert(!run(recentring, 1, 10.0, 4.0, 10.0, 4.0, now)); | ||
|
|
||
| PointerHold clipped; | ||
| assert(run(clipped, 3, 0.0, 30.0, 0.0, 2.0, now)); | ||
|
|
||
| PointerHold spaced; | ||
| for (int i = 0; i < 10; i++) { | ||
| assert(!run(spaced, 1, 50.0, 0.0, 0.0, 0.0, now)); | ||
| now += 600'000'000ul; | ||
| } | ||
|
|
||
| PointerHold hinted; | ||
| hinted.bHeld = true; | ||
| assert(run(hinted, 29, 0.0, 15.0, 0.0, 15.0, now)); | ||
| assert(!run(hinted, 1, 0.0, 15.0, 0.0, 15.0, now)); | ||
|
|
||
| PointerHold hintedCamera; | ||
| hintedCamera.bHeld = true; | ||
| assert(run(hintedCamera, 600, 30.0, -8.0, 0.0, 0.0, now)); | ||
|
|
||
| PointerHold chained; | ||
| for (int i = 0; i < 6; i++) { | ||
| assert(!run(chained, 1, 50.0, 0.0, 0.0, 0.0, now)); | ||
| now += 400'000'000ul; | ||
| } | ||
|
|
||
| PointerHold released; | ||
| assert(run(released, 3, 0.0, -25.0, 0.0, 0.0, now)); | ||
| assert(!run(released, 1, 0.0, -25.0, 0.0, 0.0, now, false)); | ||
| assert(!run(released, 2, 0.0, -25.0, 0.0, 0.0, now)); | ||
|
|
||
| puts("ok"); | ||
| return 0; | ||
| } | ||
| ''' | ||
|
|
||
|
|
||
| @unittest.skipUnless(shutil.which("c++"), "needs a C++ compiler") | ||
| class PointerHoldTest(unittest.TestCase): | ||
| def test_only_a_game_that_holds_the_x_pointer_goes_relative(self): | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| source = Path(tmp) / "hold.cpp" | ||
| binary = Path(tmp) / "hold" | ||
| source.write_text(PREAMBLE + rules() + MAIN) | ||
| subprocess.run(["c++", "-std=c++17", "-Wall", "-Wextra", "-Werror", str(source), "-o", str(binary)], check=True) | ||
| self.assertEqual(subprocess.run([str(binary)], capture_output=True, text=True, check=True).stdout.strip(), "ok") | ||
|
|
||
| def test_a_hinted_constraint_starts_relative_and_others_wait_for_the_hold(self): | ||
| added = [line.strip() for line in added_source().splitlines()] | ||
| self.assertIn("update_pointer_hold( pPaintFocus, bHasPointerConstraint && bImageEmpty && nRelativeMouse != 2 && !bExcludedAppId,", added) | ||
| self.assertIn("pPaintFocus->cursor && pPaintFocus->cursor->IsConstrained() );", added) | ||
| self.assertIn("s_Hold.bHeld = bHinted;", added) | ||
| self.assertIn("if ( s_bTracking && bHinted && !s_bHinted )", added) | ||
| self.assertIn("const bool bGameHoldsPointer = bHasPointerConstraint && s_bPointerHeld;", added) | ||
| self.assertIn(": ( bForcedRelativeMouse || s_bFollowClientPointer || ( bImageEmpty && bGameHoldsPointer && !bExcludedAppId ) );", added) | ||
|
|
||
| def test_a_new_window_or_constraint_starts_its_own_hold(self): | ||
| tracker = block(added_source(), "static void update_pointer_hold") | ||
| reset = tracker.index("if ( pWindow != s_pWindow || pConstraint != s_pConstraint )") | ||
| self.assertIn("const steamcompmgr_win_t *pWindow = pFocus->inputFocusWindow;", tracker) | ||
| self.assertIn("wlserver.GetCursorConstraint();", tracker) | ||
| self.assertLess(reset, tracker.index("if ( s_bTracking && bHinted && !s_bHinted )")) | ||
| self.assertIn("s_bTracking = false;", tracker[reset:reset + 120]) | ||
|
|
||
| def test_the_motion_sent_to_clients_is_counted(self): | ||
| added = [line.strip() for line in added_source().splitlines()] | ||
| self.assertIn("wlserver.flSentDX += unaccel_dx;", added) | ||
| self.assertIn("wlserver.flSentDY += unaccel_dy;", added) | ||
|
|
||
| def test_the_forced_and_follow_rules_are_unchanged(self): | ||
| removed = [line[1:].strip() for line in PATCH.read_text().splitlines() if line.startswith("-") and not line.startswith("---")] | ||
| self.assertEqual(removed, [": ( bForcedRelativeMouse || s_bFollowClientPointer || ( bImageEmpty && bHasPointerConstraint && !bExcludedAppId ) );"]) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve an active right-button hold during locked movement.
With direct hold set to
MOUSE_BEHAVIOR_HOLD, a long press sends right-button down. The first locked move now callscancel(), which releases that button before sending the delta. Users can no longer hold right-click while moving the locked pointer. Previously,cancelPendingHold()preserved the hold until finger-up. (raw.githubusercontent.com)Cancel the forwarded touch separately without releasing an active button hold.