diff --git a/tools/gamescope/PATCHES.md b/tools/gamescope/PATCHES.md index ab53e5ad9..5ff8197a2 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -99,3 +99,25 @@ The patch can keep the result for `DROIDDECK_WSI_BYPASS_CHECK_MS` milliseconds. or 0 keeps the per-present check; the app sets 100 only in Performance mode and 0 otherwise. With 100, a window resize or a flip/obscure change is seen up to 100 ms later than before; surface creation and capability queries still check every time. + +## 0118 Wayland: preserve signed plane positions + +Fill scaling and Steam magnification can place the image at negative x/y coordinates. +The Wayland backend converted these signed positions through an unsigned size helper: +at host scale 120, -120 became 35791275 and the image moved off-screen while overlays +could remain visible. A separate signed conversion uses a 64-bit intermediate and +retains the existing ceiling convention; viewport sizes keep their original helper. + +The component build runs `wayland_scaling` for negative crop offsets, positive/zero +positions, fractional host scales and wide intermediates. This patch may be worth +upstreaming: the same unsigned position calls were present in ValveSoftware/gamescope +3.16.29 and master `d175688ad4ec490dc7b0695c5c62f8807684a1f1` when checked on +2026-10-26. Carry it here until an upstream release containing the fix is adopted. + +Device validation on AYN Thor (1280x720 session, 1920x1080 Android output): +800x600 and 1600x600 source windows retained their image through Auto, Fit, +Stretch, Fill, Integer and 2x Steam magnification, including returning to Fit. +The old component lost the image in Fill and magnification. The tested executable +matches the pinned p14 archive and the locally assembled APK. Hardware checks used +native Gamescope properties; the physical Back+LB shortcut was not independently +confirmed. The original APK and runtime were restored after validation. diff --git a/tools/gamescope/build-in-arch.sh b/tools/gamescope/build-in-arch.sh index 79d4fd869..ba3b1605d 100755 --- a/tools/gamescope/build-in-arch.sh +++ b/tools/gamescope/build-in-arch.sh @@ -39,7 +39,7 @@ else fi PREP makepkg -A -s --noconfirm --skipchecksums --skippgpcheck -meson test -C src/build --no-rebuild --print-errorlogs cursor_routing +meson test -C src/build --no-rebuild --print-errorlogs cursor_routing wayland_scaling ls -l *.pkg.tar.* " cd "$WORK" diff --git a/tools/gamescope/patches/0118-wayland-signed-plane-positions.patch b/tools/gamescope/patches/0118-wayland-signed-plane-positions.patch new file mode 100644 index 000000000..d66ca56e0 --- /dev/null +++ b/tools/gamescope/patches/0118-wayland-signed-plane-positions.patch @@ -0,0 +1,106 @@ +diff --git a/src/Backends/WaylandBackend.cpp b/src/Backends/WaylandBackend.cpp +index d00644f..40164ba 100644 +--- a/src/Backends/WaylandBackend.cpp ++++ b/src/Backends/WaylandBackend.cpp +@@ -11,6 +11,7 @@ + #include "waitable.h" + #include "Utils/TempFiles.h" + #include "Utils/CursorRouting.h" ++#include "Utils/WaylandScaling.h" + + #include + #include +@@ -1618,8 +1619,8 @@ namespace gamescope + { + wl_subsurface_set_position( + m_pSubsurface, +- WaylandScaleToLogical( oState->nDestX, uScale ), +- WaylandScaleToLogical( oState->nDestY, uScale ) ); ++ gamescope::WaylandPositionToLogical( oState->nDestX, uScale ), ++ gamescope::WaylandPositionToLogical( oState->nDestY, uScale ) ); + } + // The x/y here does nothing? Why? What is it for... + // Use the subsurface set_position thing instead. +diff --git a/src/Utils/WaylandScaling.h b/src/Utils/WaylandScaling.h +new file mode 100644 +index 0000000..d488e39 +--- /dev/null ++++ b/src/Utils/WaylandScaling.h +@@ -0,0 +1,12 @@ ++#pragma once ++ ++#include ++ ++namespace gamescope ++{ ++ constexpr int32_t WaylandPositionToLogical( int32_t nValue, uint32_t uFactor ) ++ { ++ const int64_t nScaled = int64_t( nValue ) * 120; ++ return int32_t( nScaled / uFactor + ( nScaled % uFactor > 0 ) ); ++ } ++} +diff --git a/tests/meson.build b/tests/meson.build +index 1ccdf7d..d86bf9e 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_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'], ++ 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', 'test_wayland_scaling.cpp'], + include_directories: [srcdir, sol2_include], + dependencies: [catch2_dep, luajit_dep, cap_dep, drm_dep, glm_dep, wlroots_dep], + cpp_args: gamescope_cpp_args, +@@ -18,6 +18,7 @@ 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('wayland_scaling', gamescope_tests, args: ['[wayland_scaling]']) + 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_wayland_scaling.cpp b/tests/test_wayland_scaling.cpp +new file mode 100644 +index 0000000..498f55b +--- /dev/null ++++ b/tests/test_wayland_scaling.cpp +@@ -0,0 +1,38 @@ ++#include ++ ++#include "Utils/WaylandScaling.h" ++ ++#include ++#include ++ ++using namespace gamescope; ++ ++TEST_CASE("cropped Wayland planes retain negative positions", "[wayland_scaling]") ++{ ++ REQUIRE( WaylandPositionToLogical( -120, 120 ) == -120 ); ++ REQUIRE( WaylandPositionToLogical( -640, 120 ) == -640 ); ++ REQUIRE( WaylandPositionToLogical( -360, 120 ) == -360 ); ++ REQUIRE( WaylandPositionToLogical( 0, 120 ) == 0 ); ++ REQUIRE( WaylandPositionToLogical( 160, 120 ) == 160 ); ++} ++ ++TEST_CASE("Wayland positions round up at fractional host scales", "[wayland_scaling]") ++{ ++ const int32_t positions[] = { -100000000, -1000000, -640, -120, -1, 0, 1, 120, 640, 1000000, 100000000 }; ++ const uint32_t scales[] = { 120, 150, 180, 240, 360, 480 }; ++ for ( uint32_t uScale : scales ) ++ { ++ for ( int32_t nPosition : positions ) ++ { ++ CAPTURE( nPosition, uScale ); ++ const int32_t nExpected = int32_t( std::ceil( (long double)( nPosition ) * 120 / uScale ) ); ++ REQUIRE( WaylandPositionToLogical( nPosition, uScale ) == nExpected ); ++ } ++ } ++} ++ ++TEST_CASE("Wayland position conversion widens before multiplying", "[wayland_scaling]") ++{ ++ REQUIRE( WaylandPositionToLogical( std::numeric_limits::min(), 120 ) == std::numeric_limits::min() ); ++ REQUIRE( WaylandPositionToLogical( std::numeric_limits::max(), 120 ) == std::numeric_limits::max() ); ++} diff --git a/tools/gamescope/release.env b/tools/gamescope/release.env index a505f40a8..9bb47de0b 100644 --- a/tools/gamescope/release.env +++ b/tools/gamescope/release.env @@ -1,3 +1,3 @@ GAMESCOPE_REPO=Droid-Deck/DroidDeck-Components -GAMESCOPE_TAG=gamescope-3.16.29-p13 -GAMESCOPE_SHA256=93c789dc9e08828496f1caa2849d56acdf205838de723a24b296547a29c0b6af +GAMESCOPE_TAG=gamescope-3.16.29-p14 +GAMESCOPE_SHA256=f0f2f33767c883bcfe66f28c303fddc1564f91a915fb7cedd31eb823570b254c