From e8aaae1343841b9c2c9e618cdbaa6f6290c4d046 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Fri, 25 Sep 2026 15:55:26 -0400 Subject: [PATCH 1/5] Touch: a lost finger-up no longer blocks every later touch On the pointer path (a client that takes no wl_touch - gamescope), touch_down counted the fingers still down before clearing a sequence whose up had been lost, so the stale point left any_active set and every later tap or swipe was dropped: in Direct touch mode Big Picture ignored the screen while the pad worked. The count is taken after the clean-up, and the app ends whatever the compositor still holds when a new gesture's first finger lands (Android has ended every earlier gesture by then). The compositor logs once per gesture whether the window got touch or the pointer. --- app/src/main/cpp/waylandcomp/src/compositor.c | 15 ++++++++++++--- .../com/droiddeck/launcher/SessionActivity.kt | 8 +++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index aba2baa58..6fb0d32c0 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -3210,23 +3210,32 @@ static void touch_down(int id, double x, double y) { struct surface *target = touch_target_at(x, y); if (!target) return; struct wl_client *client = wl_resource_get_client(target->resource); + /* A down on an id that is still held means that sequence's up was lost: end it first. The + * fingers still down are counted only after that - counted before, a lost up left any_active + * set, and on the pointer path every later touch was dropped (a tap or swipe did nothing). */ + if (g_touch_points[id].surface) touch_cancel_client(client); int any_active = 0; for (int i = 0; i < MAX_TOUCH_POINTS; i++) { struct surface *active = g_touch_points[i].surface; if (!active) continue; - any_active = 1; if (active != target) { touch_cancel_all(); any_active = 0; break; } + any_active = 1; } - if (g_touch_points[id].surface) touch_cancel_client(client); struct seat_touch *st; int has_touch = 0; for_each_touch_of(client, st) has_touch = 1; + /* One line per gesture (first finger), so a test can see where a touch went. */ + if (!any_active) { + char name[160]; + describe(target, name, sizeof(name)); + banner_log("touch", "%s gets %s", name, has_touch ? "touch" : "the pointer (it takes no touch)"); + } if (!has_touch) { - if (any_active) return; + if (any_active) return; /* one finger drives the pointer; a second one is ignored */ g_touch_points[id].surface = target; g_touch_points[id].pointer_fallback = 1; pointer_event(x, y, BTN_LEFT, 1); diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index c797f1ad9..4e208eeed 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -1231,7 +1231,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { WaylandCompositor.nativeSendTouch(action, event.getPointerId(index), (x * 1919f).toInt(), (y * 1079f).toInt()) } when (event.actionMasked) { - MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> sendTouch(0, event.actionIndex) + // A first finger starts a new gesture, and Android has ended every earlier one: whatever + // the compositor still holds (an up lost to the drawer opening mid-touch) goes first. + MotionEvent.ACTION_DOWN -> { + WaylandCompositor.nativeSendTouch(3, -1, 0, 0) + sendTouch(0, event.actionIndex) + } + MotionEvent.ACTION_POINTER_DOWN -> sendTouch(0, event.actionIndex) MotionEvent.ACTION_MOVE -> for (index in 0 until event.pointerCount) sendTouch(1, index) MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> sendTouch(2, event.actionIndex) MotionEvent.ACTION_CANCEL -> WaylandCompositor.nativeSendTouch(3, -1, 0, 0) From ed1f8dd2b6760e43bfa8435101044d24efc00af5 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Fri, 25 Sep 2026 15:55:26 -0400 Subject: [PATCH 2/5] gamescope: take the host's touch in the nested Wayland backend 3.16.29's Wayland backend bound only wl_pointer and wl_keyboard; touch from DroidDeck's compositor was never read, so Steam could not be touched. 0110 binds wl_touch in the input thread and feeds each finger to wlserver_touchdown/touchmotion/touchup, the Steam Deck touchscreen's path, so Big Picture's Passthrough touch mode gets real touches. --- tools/gamescope/PATCHES.md | 6 + .../patches/0110-wayland-backend-touch.patch | 121 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 tools/gamescope/patches/0110-wayland-backend-touch.patch diff --git a/tools/gamescope/PATCHES.md b/tools/gamescope/PATCHES.md index e066ff8b8..d3e692d42 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -16,6 +16,12 @@ library list, before anything is published. 3.16.29: realtime-priority Vulkan queues on request (`GAMESCOPE_FORCE_VULKAN_REALTIME=1`) without CAP_SYS_NICE, which proot can never have; and the gamepad-driven cursor sprite following the X pointer that XTest moves (it sat frozen). +- `0110-wayland-backend-touch.patch` - this app: the nested Wayland backend bound only the host's + pointer and keyboard, so a finger on the phone's screen never reached Steam. It now binds + `wl_touch` too and hands each finger to wlserver's touch path (`wlserver_touchdown` / `motion` / + `up`) - the one a Steam Deck's touchscreen drives - so what a touch does follows the client's + touch mode (Steam's Big Picture sets Passthrough: a real touch, rows scroll under a finger). + Finger ids are offset by one, since the nested pointer already moves wlserver's touch 0. Sixteen more of Armada's patches are DRM/lease/HDR-on-KMS work for a native display, which this app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the runtime has. diff --git a/tools/gamescope/patches/0110-wayland-backend-touch.patch b/tools/gamescope/patches/0110-wayland-backend-touch.patch new file mode 100644 index 000000000..5a270e749 --- /dev/null +++ b/tools/gamescope/patches/0110-wayland-backend-touch.patch @@ -0,0 +1,121 @@ +diff --git a/src/Backends/WaylandBackend.cpp b/src/Backends/WaylandBackend.cpp +index d74268b..bf72267 100644 +--- a/src/Backends/WaylandBackend.cpp ++++ b/src/Backends/WaylandBackend.cpp +@@ -601,6 +601,17 @@ namespace gamescope + void Wayland_Keyboard_RepeatInfo( wl_keyboard *pKeyboard, int32_t nRate, int32_t nDelay ); + static const wl_keyboard_listener s_KeyboardListener; + ++ // Touch from the host compositor (DroidDeck): each finger goes to wlserver's touch path, ++ // the one a Steam Deck's touchscreen drives, instead of being dropped. ++ void Wayland_Touch_Down( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, wl_surface *pSurface, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ); ++ void Wayland_Touch_Up( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, int32_t nId ); ++ void Wayland_Touch_Motion( wl_touch *pTouch, uint32_t uTime, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ); ++ void Wayland_Touch_Frame( wl_touch *pTouch ); ++ void Wayland_Touch_Cancel( wl_touch *pTouch ); ++ static const wl_touch_listener s_TouchListener; ++ bool TouchToOutput( wl_surface *pSurface, wl_fixed_t fX, wl_fixed_t fY, double *pflX, double *pflY ); ++ std::unordered_map m_TouchSurfaces; ++ + void Wayland_RelativePointer_RelativeMotion( zwp_relative_pointer_v1 *pRelativePointer, uint32_t uTimeHi, uint32_t uTimeLo, wl_fixed_t fDx, wl_fixed_t fDy, wl_fixed_t fDxUnaccel, wl_fixed_t fDyUnaccel ); + static const zwp_relative_pointer_v1_listener s_RelativePointerListener; + }; +@@ -636,6 +647,16 @@ namespace gamescope + .modifiers = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Keyboard_Modifiers ), + .repeat_info = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Keyboard_RepeatInfo ), + }; ++ const wl_touch_listener CWaylandInputThread::s_TouchListener = ++ { ++ .down = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Touch_Down ), ++ .up = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Touch_Up ), ++ .motion = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Touch_Motion ), ++ .frame = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Touch_Frame ), ++ .cancel = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Touch_Cancel ), ++ .shape = WAYLAND_NULL(), ++ .orientation = WAYLAND_NULL(), ++ }; + const zwp_relative_pointer_v1_listener CWaylandInputThread::s_RelativePointerListener = + { + .relative_motion = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_RelativePointer_RelativeMotion ), +@@ -3067,6 +3088,81 @@ namespace gamescope + wl_keyboard_add_listener( m_pKeyboard, &s_KeyboardListener, this ); + } + } ++ ++ if ( !!( uCapabilities & WL_SEAT_CAPABILITY_TOUCH ) != !!m_pTouch ) ++ { ++ if ( m_pTouch ) ++ { ++ wl_touch_release( m_pTouch ); ++ m_pTouch = nullptr; ++ m_TouchSurfaces.clear(); ++ } ++ else ++ { ++ m_pTouch = wl_seat_get_touch( m_pSeat ); ++ wl_touch_add_listener( m_pTouch, &s_TouchListener, this ); ++ } ++ } ++ } ++ ++ // Touch ++ ++ bool CWaylandInputThread::TouchToOutput( wl_surface *pSurface, wl_fixed_t fX, wl_fixed_t fY, double *pflX, double *pflY ) ++ { ++ if ( !pSurface || !IsGamescopeToplevel( pSurface ) ) ++ return false; ++ CWaylandPlane *pPlane = (CWaylandPlane *)wl_surface_get_user_data( pSurface ); ++ if ( !pPlane ) ++ return false; ++ auto oState = pPlane->GetCurrentState(); ++ if ( !oState || !g_nOutputWidth || !g_nOutputHeight ) ++ return false; ++ uint32_t uScale = oState->uFractionalScale; ++ *pflX = ( wl_fixed_to_double( fX ) * uScale / 120.0 + oState->nDestX ) / g_nOutputWidth; ++ *pflY = ( wl_fixed_to_double( fY ) * uScale / 120.0 + oState->nDestY ) / g_nOutputHeight; ++ return true; ++ } ++ // Touch ids are offset by one: the nested pointer already moves wlserver's touch 0. ++ void CWaylandInputThread::Wayland_Touch_Down( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, wl_surface *pSurface, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ) ++ { ++ double flX, flY; ++ if ( !TouchToOutput( pSurface, fX, fY, &flX, &flY ) ) ++ return; ++ m_TouchSurfaces[ nId ] = pSurface; ++ wlserver_lock(); ++ wlserver_touchdown( flX, flY, nId + 1, ++m_uFakeTimestamp ); ++ wlserver_unlock(); ++ } ++ void CWaylandInputThread::Wayland_Touch_Up( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, int32_t nId ) ++ { ++ if ( m_TouchSurfaces.erase( nId ) == 0 ) ++ return; ++ wlserver_lock(); ++ wlserver_touchup( nId + 1, ++m_uFakeTimestamp ); ++ wlserver_unlock(); ++ } ++ void CWaylandInputThread::Wayland_Touch_Motion( wl_touch *pTouch, uint32_t uTime, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ) ++ { ++ auto it = m_TouchSurfaces.find( nId ); ++ if ( it == m_TouchSurfaces.end() ) ++ return; ++ double flX, flY; ++ if ( !TouchToOutput( it->second, fX, fY, &flX, &flY ) ) ++ return; ++ wlserver_lock(); ++ wlserver_touchmotion( flX, flY, nId + 1, ++m_uFakeTimestamp ); ++ wlserver_unlock(); ++ } ++ void CWaylandInputThread::Wayland_Touch_Frame( wl_touch *pTouch ) ++ { ++ } ++ void CWaylandInputThread::Wayland_Touch_Cancel( wl_touch *pTouch ) ++ { ++ wlserver_lock(); ++ for ( auto &[ nId, pSurface ] : m_TouchSurfaces ) ++ wlserver_touchup( nId + 1, ++m_uFakeTimestamp ); ++ wlserver_unlock(); ++ m_TouchSurfaces.clear(); + } + + void CWaylandInputThread::Wayland_Seat_Name( wl_seat *pSeat, const char *pName ) From a8af9737266448ebbfec1bf279cfaf226686e79f Mon Sep 17 00:00:00 2001 From: The412Banner Date: Fri, 25 Sep 2026 16:06:31 -0400 Subject: [PATCH 3/5] gamescope: stage 3.16.29-p2 (0110, touch from the host) --- tools/gamescope/release.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/gamescope/release.env b/tools/gamescope/release.env index ce7b2552f..61a48281a 100644 --- a/tools/gamescope/release.env +++ b/tools/gamescope/release.env @@ -1,2 +1,2 @@ -GAMESCOPE_TAG=gamescope-3.16.29-p1 -GAMESCOPE_SHA256=522d1835d84e4a64a843e095bbe341f68b10f8467f38c0e72ae4ef20b675b873 +GAMESCOPE_TAG=gamescope-3.16.29-p2 +GAMESCOPE_SHA256=f20482419ad17fb1b43aa9d8ca16b22e5ad936f7c77c58184d9ecc0a9f38be95 From cc3fec0768a6103e4b0253e93a0acab6dd6f5069 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Fri, 25 Sep 2026 16:34:22 -0400 Subject: [PATCH 4/5] gamescope: the nested pointer moves Steam's pointer in Passthrough too In touchpad mode the cursor moved but Big Picture highlighted nothing and a tap-to-click selected nothing: the nested pointer's motion reaches wlserver as touch 0, and in Passthrough (the touch mode Big Picture sets) a motion for a touch that is not down moves nothing, so the X pointer stayed where it was. 0111 passes bAlwaysWarpCursor, as the other touch modes already warp. --- tools/gamescope/PATCHES.md | 5 +++++ ...11-wayland-pointer-warps-in-passthrough.patch | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch diff --git a/tools/gamescope/PATCHES.md b/tools/gamescope/PATCHES.md index d3e692d42..8fa264904 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -22,6 +22,11 @@ library list, before anything is published. `up`) - the one a Steam Deck's touchscreen drives - so what a touch does follows the client's touch mode (Steam's Big Picture sets Passthrough: a real touch, rows scroll under a finger). Finger ids are offset by one, since the nested pointer already moves wlserver's touch 0. +- `0111-wayland-pointer-warps-in-passthrough.patch` - this app: the nested pointer's motion goes to + wlserver as touch 0, and in Passthrough (Big Picture's touch mode) a motion for a touch that is not + down moves nothing - so in the app's touchpad mode the Steam client saw no hover and a click landed + wherever the pointer had last been. The motion now always warps the real pointer as well + (`bAlwaysWarpCursor`), which the other touch modes did already. Sixteen more of Armada's patches are DRM/lease/HDR-on-KMS work for a native display, which this app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the runtime has. diff --git a/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch b/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch new file mode 100644 index 000000000..466d0a781 --- /dev/null +++ b/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch @@ -0,0 +1,16 @@ +diff --git a/src/Backends/WaylandBackend.cpp b/src/Backends/WaylandBackend.cpp +index bf72267..c400978 100644 +--- a/src/Backends/WaylandBackend.cpp ++++ b/src/Backends/WaylandBackend.cpp +@@ -3223,7 +3223,10 @@ namespace gamescope + double flY = ( wl_fixed_to_double( fSurfaceY ) * uScale / 120.0 + oState->nDestY ) / g_nOutputHeight; + + wlserver_lock(); +- wlserver_touchmotion( flX, flY, 0, ++m_uFakeTimestamp ); ++ // Always move the real pointer too: in Passthrough (what Steam's Big Picture asks for) a ++ // touch motion for an id that is not down moves nothing, so hover never reached the ++ // client and a click landed wherever the pointer had last been. ++ wlserver_touchmotion( flX, flY, 0, ++m_uFakeTimestamp, true ); + wlserver_unlock(); + } + void CWaylandInputThread::Wayland_Pointer_Button( wl_pointer *pPointer, uint32_t uSerial, uint32_t uTime, uint32_t uButton, uint32_t uState ) From 1dc49b527bc13d405bd80966f1a857bf6a5a344e Mon Sep 17 00:00:00 2001 From: The412Banner Date: Fri, 25 Sep 2026 16:38:50 -0400 Subject: [PATCH 5/5] gamescope: stage 3.16.29-p3 (0111, pointer warps in Passthrough) --- tools/gamescope/release.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/gamescope/release.env b/tools/gamescope/release.env index 61a48281a..a7a39e459 100644 --- a/tools/gamescope/release.env +++ b/tools/gamescope/release.env @@ -1,2 +1,2 @@ -GAMESCOPE_TAG=gamescope-3.16.29-p2 -GAMESCOPE_SHA256=f20482419ad17fb1b43aa9d8ca16b22e5ad936f7c77c58184d9ecc0a9f38be95 +GAMESCOPE_TAG=gamescope-3.16.29-p3 +GAMESCOPE_SHA256=566e3b93931f37c30375d440d15ee76452892b842d2dc854cd3c06b926c8451b