mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-15 03:37:09 -04:00
Show controller-driven cursors in nested Wayland sessions (#759)
This commit is contained in:
1 parent
d99d728ba5
commit
799be037e5
6 files changed
+250
-2
No files matched your search
@@ -19,6 +19,11 @@ library list, before anything is published.
|
||||
moves (it sat frozen). The X pointer is asked for only while a cursor image is drawn - every
|
||||
vblank while shown, every 50 ms while hidden for inactivity - since each ask is a blocking round
|
||||
trip to Xwayland on the paint thread; with no image, wlserver's position is used as upstream does.
|
||||
- `0101-nested-wayland-cursor-routing.patch` - this app: `gamescope_cursor_composite=2` lets the
|
||||
nested Wayland session paint its cursor in the output using the real X pointer position; client
|
||||
cursor surfaces are cleared to prevent a duplicate at the host pointer, while Gamescope's original
|
||||
default host-cursor fallback is preserved when its no-keyboard-focus policy selects it. Empty
|
||||
client cursor images and Gamescope's hidden flag still suppress the painted cursor.
|
||||
- `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` /
|
||||
|
||||
@@ -39,6 +39,7 @@ else
|
||||
fi
|
||||
PREP
|
||||
makepkg -A -s --noconfirm --skipchecksums --skippgpcheck
|
||||
meson test -C src/build --no-rebuild --print-errorlogs cursor_routing
|
||||
ls -l *.pkg.tar.*
|
||||
"
|
||||
cd "$WORK"
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
diff --git a/src/Backends/WaylandBackend.cpp b/src/Backends/WaylandBackend.cpp
|
||||
index d74268b..f49ef59 100644
|
||||
--- a/src/Backends/WaylandBackend.cpp
|
||||
+++ b/src/Backends/WaylandBackend.cpp
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "refresh_rate.h"
|
||||
#include "waitable.h"
|
||||
#include "Utils/TempFiles.h"
|
||||
+#include "Utils/CursorRouting.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
@@ -114,6 +115,7 @@ static bool IsGamescopeToplevel( wl_surface *pSurface ) {
|
||||
#define LIBDECOR_USERDATA_TO_THIS(type, name) []<typename... Args> ( Args... args ) { type *pThing = (type *)std::get<sizeof...(Args)-1>(std::forward_as_tuple(args...)); CallWithAllButLast([&]<typename... Args2>(Args2... args2){ pThing->name(std::forward<Args2>(args2)...); }, std::forward<Args>(args)...); }
|
||||
|
||||
extern gamescope::ConVar<bool> cv_hdr_enabled;
|
||||
+extern gamescope::ConVar<int> cv_cursor_composite;
|
||||
|
||||
namespace gamescope
|
||||
{
|
||||
@@ -2462,11 +2464,16 @@ namespace gamescope
|
||||
if ( !m_pPointer )
|
||||
return;
|
||||
|
||||
- if ( cv_wayland_mouse_warp_without_keyboard_focus )
|
||||
- bUseHostCursor = m_bPointerLocked && !m_bKeyboardEntered && m_pDefaultCursorSurface;
|
||||
- else
|
||||
- bUseHostCursor = !m_bKeyboardEntered && m_pDefaultCursorSurface;
|
||||
-
|
||||
+ bUseHostCursor = CursorShouldUseHostFallback(
|
||||
+ cv_wayland_mouse_warp_without_keyboard_focus.Get(), m_bPointerLocked,
|
||||
+ m_bKeyboardEntered, m_pDefaultCursorSurface != nullptr );
|
||||
+
|
||||
+ if ( !CursorShouldBeForwardedToHost( cv_cursor_composite.Get(), bUseHostCursor ) )
|
||||
+ {
|
||||
+ wl_pointer_set_cursor( m_pPointer, m_uPointerEnterSerial, nullptr, 0, 0 );
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
if ( bUseHostCursor )
|
||||
{
|
||||
wl_pointer_set_cursor( m_pPointer, m_uPointerEnterSerial, m_pDefaultCursorSurface, m_pDefaultCursorInfo->uXHotspot, m_pDefaultCursorInfo->uYHotspot );
|
||||
diff --git a/src/Utils/CursorRouting.h b/src/Utils/CursorRouting.h
|
||||
new file mode 100644
|
||||
index 0000000..d7c21da
|
||||
--- /dev/null
|
||||
+++ b/src/Utils/CursorRouting.h
|
||||
@@ -0,0 +1,36 @@
|
||||
+#pragma once
|
||||
+
|
||||
+namespace gamescope
|
||||
+{
|
||||
+ constexpr bool CursorShouldBeComposited( int cursorCompositeMode, bool forceRelativeMouse,
|
||||
+ bool hasFocus, bool hasNestedHints, bool nestedPaintsCursor )
|
||||
+ {
|
||||
+ if ( cursorCompositeMode == 2 )
|
||||
+ return true;
|
||||
+ if ( cursorCompositeMode == 0 )
|
||||
+ return false;
|
||||
+ if ( forceRelativeMouse )
|
||||
+ return true;
|
||||
+ if ( !hasFocus )
|
||||
+ return false;
|
||||
+ return !hasNestedHints || nestedPaintsCursor;
|
||||
+ }
|
||||
+
|
||||
+ constexpr bool CursorShouldUseHostFallback( bool warpWithoutKeyboardFocus, bool pointerLocked,
|
||||
+ bool keyboardEntered, bool hasDefaultCursor )
|
||||
+ {
|
||||
+ if ( warpWithoutKeyboardFocus )
|
||||
+ return pointerLocked && !keyboardEntered && hasDefaultCursor;
|
||||
+ return !keyboardEntered && hasDefaultCursor;
|
||||
+ }
|
||||
+
|
||||
+ constexpr bool CursorShouldBeForwardedToHost( int cursorCompositeMode, bool useHostCursorFallback )
|
||||
+ {
|
||||
+ return cursorCompositeMode != 2 || useHostCursorFallback;
|
||||
+ }
|
||||
+
|
||||
+ constexpr bool CursorImageShouldBePainted( bool imageEmpty, bool cursorHidden )
|
||||
+ {
|
||||
+ return !imageEmpty && !cursorHidden;
|
||||
+ }
|
||||
+}
|
||||
diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp
|
||||
index 18b799e..2e90466 100644
|
||||
--- a/src/steamcompmgr.cpp
|
||||
+++ b/src/steamcompmgr.cpp
|
||||
@@ -99,6 +99,7 @@
|
||||
#include "BufferMemo.h"
|
||||
#include "vrclient_detect.h"
|
||||
#include "Utils/Process.h"
|
||||
+#include "Utils/CursorRouting.h"
|
||||
|
||||
#include "wlr_begin.hpp"
|
||||
#include "wlr/types/wlr_pointer_constraints_v1.h"
|
||||
@@ -2353,7 +2354,7 @@ void MouseCursor::GetDesiredSize( int& nWidth, int &nHeight )
|
||||
|
||||
void MouseCursor::paint(steamcompmgr_win_t *window, steamcompmgr_win_t *fit, struct FrameInfo_t *frameInfo)
|
||||
{
|
||||
- if ( m_imageEmpty || wlserver.bCursorHidden )
|
||||
+ if ( !gamescope::CursorImageShouldBePainted( m_imageEmpty, wlserver.bCursorHidden ) )
|
||||
return;
|
||||
|
||||
int winX = x();
|
||||
@@ -3025,23 +3026,11 @@ static void paint_pipewire()
|
||||
gamescope::ConVar<int> cv_cursor_composite{ "cursor_composite", 1, "0 = Never composite a cursor. 1 = Composite cursor when not nested. 2 = Always composite a cursor manually" };
|
||||
bool ShouldDrawCursor()
|
||||
{
|
||||
- if ( cv_cursor_composite == 2 )
|
||||
- return true;
|
||||
-
|
||||
- if ( cv_cursor_composite == 0 )
|
||||
- return false;
|
||||
-
|
||||
- if ( g_bForceRelativeMouse )
|
||||
- return true;
|
||||
-
|
||||
global_focus_t *pFocus = GetCurrentFocus();
|
||||
- if ( !pFocus )
|
||||
- return false;
|
||||
-
|
||||
- if ( !pFocus->GetNestedHints() )
|
||||
- return true;
|
||||
-
|
||||
- return pFocus->GetNestedHints()->ShouldPaintCursor();
|
||||
+ gamescope::INestedHints *pNestedHints = pFocus ? pFocus->GetNestedHints() : nullptr;
|
||||
+ return gamescope::CursorShouldBeComposited( cv_cursor_composite.Get(), g_bForceRelativeMouse,
|
||||
+ pFocus != nullptr, pNestedHints != nullptr,
|
||||
+ pNestedHints && pNestedHints->ShouldPaintCursor() );
|
||||
}
|
||||
|
||||
static void ForwardVROverlayTargets()
|
||||
diff --git a/tests/meson.build b/tests/meson.build
|
||||
index 9f73a33..1ccdf7d 100644
|
||||
--- a/tests/meson.build
|
||||
+++ b/tests/meson.build
|
||||
@@ -8,7 +8,7 @@ endforeach
|
||||
|
||||
gamescope_tests = executable(
|
||||
'gamescope_tests',
|
||||
- unittest_src + ['test_convar.cpp', 'test_focus_placement.cpp', 'test_mangoapp_config.cpp', 'test_mangoapp_control.cpp', 'test_process.cpp', 'test_upscale.cpp', 'test_utils_parsers.cpp', 'test_utils_string.cpp', 'test_vrclient_detect.cpp'],
|
||||
+ unittest_src + ['test_convar.cpp', 'test_cursor_routing.cpp', 'test_focus_placement.cpp', 'test_mangoapp_config.cpp', 'test_mangoapp_control.cpp', 'test_process.cpp', 'test_upscale.cpp', 'test_utils_parsers.cpp', 'test_utils_string.cpp', 'test_vrclient_detect.cpp'],
|
||||
include_directories: [srcdir, sol2_include],
|
||||
dependencies: [catch2_dep, luajit_dep, cap_dep, drm_dep, glm_dep, wlroots_dep],
|
||||
cpp_args: gamescope_cpp_args,
|
||||
@@ -17,6 +17,7 @@ gamescope_tests = executable(
|
||||
reaper_test_helper = executable('reaper_test_helper', 'reaper_test_helper.cpp')
|
||||
|
||||
test('convar', gamescope_tests, args: ['[convar]'])
|
||||
+test('cursor_routing', gamescope_tests, args: ['[cursor_routing]'])
|
||||
test('focus_placement', gamescope_tests, args: ['[focus_placement]'])
|
||||
test('mangoapp_config', gamescope_tests, args: ['[mangoapp_config]'])
|
||||
test('mangoapp_control', gamescope_tests, args: ['[mangoapp_control]'])
|
||||
diff --git a/tests/test_cursor_routing.cpp b/tests/test_cursor_routing.cpp
|
||||
new file mode 100644
|
||||
index 0000000..9c622db
|
||||
--- /dev/null
|
||||
+++ b/tests/test_cursor_routing.cpp
|
||||
@@ -0,0 +1,42 @@
|
||||
+#include <catch2/catch_test_macros.hpp>
|
||||
+
|
||||
+#include "Utils/CursorRouting.h"
|
||||
+
|
||||
+using namespace gamescope;
|
||||
+
|
||||
+TEST_CASE("nested cursor composition can be forced without changing the default", "[cursor_routing]")
|
||||
+{
|
||||
+ REQUIRE_FALSE( CursorShouldBeComposited( 1, false, true, true, false ) );
|
||||
+ REQUIRE( CursorShouldBeComposited( 2, false, true, true, false ) );
|
||||
+
|
||||
+ REQUIRE( CursorShouldBeComposited( 1, true, true, true, false ) );
|
||||
+ REQUIRE( CursorShouldBeComposited( 1, false, true, false, false ) );
|
||||
+ REQUIRE_FALSE( CursorShouldBeComposited( 0, true, true, false, true ) );
|
||||
+ REQUIRE_FALSE( CursorShouldBeComposited( 1, false, false, false, false ) );
|
||||
+ REQUIRE( CursorShouldBeComposited( 2, false, false, true, false ) );
|
||||
+}
|
||||
+
|
||||
+TEST_CASE("manual composition retains only the no-focus default cursor fallback", "[cursor_routing]")
|
||||
+{
|
||||
+ REQUIRE_FALSE( CursorShouldBeForwardedToHost( 2, false ) );
|
||||
+ REQUIRE( CursorShouldBeForwardedToHost( 2, true ) );
|
||||
+ REQUIRE( CursorShouldBeForwardedToHost( 1, false ) );
|
||||
+ REQUIRE( CursorShouldBeForwardedToHost( 0, false ) );
|
||||
+}
|
||||
+
|
||||
+TEST_CASE("host cursor fallback follows the keyboard-focus and lock defaults", "[cursor_routing]")
|
||||
+{
|
||||
+ REQUIRE( CursorShouldUseHostFallback( true, true, false, true ) );
|
||||
+ REQUIRE_FALSE( CursorShouldUseHostFallback( true, false, false, true ) );
|
||||
+ REQUIRE_FALSE( CursorShouldUseHostFallback( true, true, true, true ) );
|
||||
+ REQUIRE_FALSE( CursorShouldUseHostFallback( true, true, false, false ) );
|
||||
+ REQUIRE( CursorShouldUseHostFallback( false, false, false, true ) );
|
||||
+ REQUIRE_FALSE( CursorShouldUseHostFallback( false, true, true, true ) );
|
||||
+}
|
||||
+
|
||||
+TEST_CASE("an empty or hidden client cursor stays hidden when composition is forced", "[cursor_routing]")
|
||||
+{
|
||||
+ REQUIRE_FALSE( CursorImageShouldBePainted( true, false ) );
|
||||
+ REQUIRE_FALSE( CursorImageShouldBePainted( false, true ) );
|
||||
+ REQUIRE( CursorImageShouldBePainted( false, false ) );
|
||||
+}
|
||||
@@ -1,3 +1,3 @@
|
||||
GAMESCOPE_REPO=Droid-Deck/DroidDeck-Components
|
||||
GAMESCOPE_TAG=gamescope-3.16.29-p10
|
||||
GAMESCOPE_SHA256=e8e2433ae618458135ec271a30af7e80b282ff41f9931286e9bbad614838eca0
|
||||
GAMESCOPE_TAG=gamescope-3.16.29-cursor-045e5a31-qa
|
||||
GAMESCOPE_SHA256=76929e93804450e7352421b76a457c825a6251de8b704d942c363a211bd99424
|
||||
@@ -196,6 +196,9 @@ if [ -z "${BL_INSIDE:-}" ]; then
|
||||
echo "== gamescope: games at ${nested}x${height} stretched to the ${width}x${height} output"
|
||||
fi
|
||||
fi
|
||||
if [ "$mode" = steam ]; then
|
||||
export gamescope_cursor_composite=2
|
||||
fi
|
||||
args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$height")
|
||||
[ "$nested" != "$width" ] && args+=(-S stretch) && export BL_STRETCHED=1
|
||||
if [ "${BL_FPS:-0}" -gt 0 ]; then
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""The XTest cursor path is enabled only for Steam sessions."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
SESSION = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin/droiddeck-session'
|
||||
SOURCE = SESSION.read_text()
|
||||
MATCH = re.search(
|
||||
r'^ if \[ "\$mode" = steam \]; then\n.*?^ fi\n', SOURCE, re.M | re.S)
|
||||
assert MATCH is not None
|
||||
CURSOR_SETUP = MATCH.group(0)
|
||||
|
||||
|
||||
class SessionCursorRoutingTest(unittest.TestCase):
|
||||
def run_setup(self, mode, initial=None):
|
||||
env = dict(os.environ, mode=mode)
|
||||
if initial is not None:
|
||||
env['gamescope_cursor_composite'] = initial
|
||||
else:
|
||||
env.pop('gamescope_cursor_composite', None)
|
||||
result = subprocess.run(
|
||||
['bash', '-c', CURSOR_SETUP + 'printf "%s\n" "${gamescope_cursor_composite-}"'],
|
||||
env=env, text=True, capture_output=True, check=True)
|
||||
return result.stdout.strip()
|
||||
|
||||
def test_steam_enables_manual_composition_for_the_nested_cursor(self):
|
||||
self.assertEqual('2', self.run_setup('steam'))
|
||||
|
||||
def test_other_sessions_keep_their_existing_cursor_composition(self):
|
||||
self.assertEqual('', self.run_setup('desktop'))
|
||||
self.assertEqual('1', self.run_setup('run', '1'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user