Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,38 @@ is missing, built from another Mesa version, or older than the sources in
`android/mesa-glamor/` — a stage directory that exists is not evidence that it
is current, and the payload's glamor is only as new as that stage. See
`android/mesa-glamor/README.md`.

### Two Mesas: glamor's and the guest's

The glamor Mesa above is Xwayland's: it lives in the payload and renders the X
server's own drawing. It is not the OpenGL library a Windows game gets. That one
is the guest libGL, `usr/lib/libGL.so.1.5.0` (with `libglapi.so.0`) in the app's
`extra_libs.tzst` archive, which the app extracts into every container at launch
and which Wine's winex11 loads (`WINE_X11FORCEGLX=1`) on BOTH display servers,
the in-app X11 server and gamescope/Xwayland: the same file, same bytes. It is
Mesa's xlib GLX emulation on zink (fbconfigs are X visuals, frames reach the X
server through XShmPutImage, no DRI), from the Pipetto xlib/zink fork: branch
`zink-mesa-xlib-pipetto` of GameNative/mesa, and the shipped file reports
`Mesa 24.3.0-devel (git-c1567e9609)`, so it carries that fork's changes up to
that commit and nothing above it. Upstream Mesa deleted the xlib GLX frontend in
26.3 (7055daf6a57), so it cannot be rebuilt from the glamor tarball.

What differs between the two display servers is therefore never the library but
the X server it asks: Xwayland's default visual is depth 24, the in-app server's
single visual is depth 32, and the emulation reports GLX_BUFFER_SIZE from the
visual depth (GameNative/mesa#58 makes it count the emulated alpha channel, so an
8/8/8/8 format says 32 on both). A guest GL frame always arrives on the X
window's own surface, not on the WSI shim's override surface, which is what A33
has to account for.

Building that libGL (no CI does it; the wrapper package script only repacks the
shipped file): the cross setup of `android/kopper/configure.sh` on GameNative/mesa
branch `zink-kopper-mali-24.3` (NDK 27.3 + a termux aarch64 sysroot) with
`-Dglx=xlib -Dgallium-drivers=zink -Dvulkan-drivers= -Degl=disabled -Dgbm=disabled
-Dplatforms=x11 -Dshared-glapi=enabled -Dglvnd=disabled -Dllvm=disabled
-Dxmlconfig=disabled -Dexpat=disabled -Dzstd=enabled`, `-fno-emulated-tls` in
`c_args`/`cpp_args` (the shipped `libglapi.so.0` exports `_glapi_tls_Context` as
ELF TLS; an emutls libGL fails to dlopen against it). Building a commit from
before Pipetto's "disable dri options query" hack (the shipped c1567e9609 is
one) also needs `idep_xmlconfig` on the `libgl-xlib` meson target, since zink's
driconf calls then need the stub; the branch head does not.
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,58 @@ to end the session; hold the output black there, and paint one black frame
before the loop stops.

The same desktop background is what the panel is left holding at the other end:
from startup until the guest's first frame arrives on the override surface,
wine's desktop is the only window the compositor has. So the blank is raised on
the texture selection itself -- is this window's newest done commit still the
override surface's? -- and not on the reaper alone, and a window that has never
had an override surface is never blanked.
from startup until the guest's first frame arrives, wine's desktop is the only
content the compositor has. So the blank is raised earlier too -- whenever the
game's window is gone while the session is still alive -- and a window that has
never had an override surface is never blanked.

What tells the desktop from the game's content is time, not surface. The first
round of this patch judged it by the surface of the window's newest done commit
(a commit from the window's own surface was taken to be the desktop), which
holds for a Vulkan guest but not for one that renders through GL: the xlib
libGL hands its frames to Xwayland with XShmPutImage, so they arrive on the
window's own surface, and such a guest may still have bound an override surface
for a Vulkan swapchain it never presents on (Tomb Raider I-III Remastered loads
dxgi/d3d11 beside OpenGL). Judged by surface, every one of its frames looked
like the desktop and the blind never lifted: audio over a black panel for the
whole session. So the binding is stamped with the window's newest done commit
id the first time the override is seen, and the startup blind holds until a
done commit newer than that arrives, from whichever surface carries the game's
frames; commit ids come from one counter, so "newer" is an id comparison. At
exit the override is gone after having been bound, which the surface alone does
say. A DXVK guest keeps the startup blank until its first presented frame (its
desktop commit precedes the swapchain); a GL guest unblanks on its first
XShmPutImage frame.

The blind's one-shot log line states the stamped id and the newest done commit
id at every transition, so the panel evidence can be tied to the comparison.

diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp
index 54e4efc..f3f914a 100644
--- a/src/rendervulkan.cpp
+++ b/src/rendervulkan.cpp
@@ -3189,9 +3189,13 @@ gamescope::OwningRc<CVulkanTexture> vulkan_create_flat_texture( uint32_t width,

gamescope::OwningRc<CVulkanTexture> vulkan_create_debug_blank_texture()
{
- // To match Steam's scaling, which is capped at 1080p
- int width = std::min<int>( g_nOutputWidth, 1920 );
- int height = std::min<int>( g_nOutputHeight, 1080 );
+ // A33: this texture is the session-end blank, painted as one full-output
+ // layer. Its extent must be the output's, not Steam's 1080p overlay cap:
+ // a capped extent is not screen size, which forces the composite path and
+ // leaves the blank's coverage resting on the shader's out-of-extent black
+ // fill instead of on the layer's own geometry.
+ int width = g_nOutputWidth;
+ int height = g_nOutputHeight;

return vulkan_create_flat_texture( width, height, 0, 0, 0, 0 );
}
diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp
index 2dfdd7f..e9ec9a7 100644
--- a/src/steamcompmgr.cpp
+++ b/src/steamcompmgr.cpp
@@ -3114,6 +3114,92 @@
@@ -3131,6 +3131,115 @@ gamescope::ConVar<bool> cv_paint_external_overlay_plane{ "paint_external_overlay
gamescope::ConVar<bool> cv_paint_cursor_plane{ "paint_cursor_plane", true };
gamescope::ConVar<bool> cv_paint_mura_plane{ "paint_mura_plane", true };

Expand Down Expand Up @@ -79,37 +122,60 @@ had an override surface is never blanked.
+// commit's own surface says which of the two content streams is being painted.
+//
+// Two properties are load-bearing and are why this is safe:
+// - it cannot be true during normal play: while the game runs the override
+// commits every frame, so the newest done commit is always the override;
+// - it cannot be true during normal play: once the game's content has
+// arrived, the newest done commit is always newer than the binding;
+// - it cannot wedge a session that never opens a second window: the latch is
+// only ever set by the override surface coming into existence, so a session
+// whose guest never brings one up is never blanked.
+//
+// The newest commit's surface alone is not the discriminator, though. A guest
+// that renders through GL hands its frames to Xwayland (the xlib libGL's
+// XShmPutImage), so they arrive on the window's own surface, while the same
+// guest may have bound an override surface for a Vulkan swapchain it never
+// presents on (Tomb Raider I-III Remastered loads dxgi/d3d11 beside OpenGL).
+// Judged by surface, its frames look like the desktop and the blind never
+// lifts. What tells the desktop from content is time, not surface: the desktop
+// is what the window had done before the override came into existence, so the
+// blind at startup holds until a commit newer than that binding is done, from
+// whichever surface carries the game's frames. At exit the override is gone
+// after having been bound, which the surface does say.
+static bool
+session_game_window_absent( steamcompmgr_win_t *w )
+{
+ if ( w == nullptr || w->type != steamcompmgr_win_type_t::XWAYLAND )
+ return false;
+
+ struct wlr_surface *override_surface = w->xwayland().surface.override_surface;
+ if ( override_surface != nullptr )
+
+ gamescope::Rc<commit_t> lastCommit;
+ get_window_last_done_commit( w, lastCommit );
+
+ if ( override_surface != nullptr && !w->bSessionSawGameWindow )
+ {
+ // First sighting of the game's content: everything done so far is the
+ // desktop. Commit ids are issued from one counter, so "newer" is an
+ // id comparison.
+ w->bSessionSawGameWindow = true;
+ w->ulSessionOverrideBoundCommitID = lastCommit ? lastCommit->commitID : 0;
+ }
+
+ if ( !w->bSessionSawGameWindow )
+ return false;
+
+ gamescope::Rc<commit_t> lastCommit;
+ get_window_last_done_commit( w, lastCommit );
+ // The override is gone after having been bound: the desktop is all that is
+ // left (the game's own exit path, ahead of the reaper).
+ if ( override_surface == nullptr )
+ return true;
+
+ // The guest's own content is what would be painted only while the newest
+ // done commit is the override's. Null override, no commit, or a commit from
+ // the window's own surface all mean the desktop is all that is left.
+ return lastCommit == nullptr || lastCommit->surf != override_surface;
+ // Startup: nothing done yet, or nothing newer than the binding, is still
+ // the desktop. The first newer commit, from either surface, is the game.
+ return lastCommit == nullptr || lastCommit->commitID <= w->ulSessionOverrideBoundCommitID;
+}
+
static void
paint_all( global_focus_t *pFocus, bool async )
{
@@ -3153,6 +3239,37 @@
@@ -3170,6 +3279,39 @@ paint_all( global_focus_t *pFocus, bool async )
// Decide the fit target once so the window planes and the cursor plane agree.
fit = override;

Expand All @@ -125,12 +191,14 @@ had an override surface is never blanked.
+ if ( ( bSessionBlank ? 1 : 0 ) != s_nLastBlankState )
+ {
+ s_nLastBlankState = bSessionBlank ? 1 : 0;
+ s_SessionBlankLogScope.infof( "blank=%d reaper=%d window=%s override_bound=%d saw_game_window=%d",
+ s_SessionBlankLogScope.infof( "blank=%d reaper=%d window=%s override_bound=%d saw_game_window=%d bound_commit=%lu last_done_commit=%lu",
+ s_nLastBlankState,
+ g_bSessionEndBlank ? 1 : 0,
+ w ? "present" : "none",
+ ( w && w->type == steamcompmgr_win_type_t::XWAYLAND && w->xwayland().surface.override_surface != nullptr ) ? 1 : 0,
+ ( w && w->type == steamcompmgr_win_type_t::XWAYLAND && w->bSessionSawGameWindow ) ? 1 : 0 );
+ ( w && w->type == steamcompmgr_win_type_t::XWAYLAND && w->bSessionSawGameWindow ) ? 1 : 0,
+ ( w && w->type == steamcompmgr_win_type_t::XWAYLAND ) ? (unsigned long)w->ulSessionOverrideBoundCommitID : 0ul,
+ (unsigned long)window_last_done_commit_id( w ) );
+ }
+
+ if ( bSessionBlank )
Expand All @@ -147,7 +215,7 @@ had an override surface is never blanked.
if (++frameCounter == 300)
{
currentFrameRate = 300 * 1000.0f / (currentTime - lastSampledFrameTime);
@@ -3184,7 +3301,11 @@
@@ -3201,7 +3343,11 @@ paint_all( global_focus_t *pFocus, bool async )
// find the video underlay and put it up first in the scenegraph
if ( cv_paint_primary_plane )
{
Expand All @@ -160,7 +228,7 @@ had an override surface is never blanked.
{
if ( w->isSteamStreamingClient == true )
{
@@ -9447,6 +9568,10 @@
@@ -9464,6 +9610,10 @@ void LaunchNestedChildren( char **ppPrimaryChildArgv )
gamescope::Process::WaitForChild( nPrimaryChildPid );
s_LaunchLogScope.infof( "Primary child shut down!" );

Expand All @@ -171,7 +239,7 @@ had an override surface is never blanked.
if ( cv_shutdown_on_primary_child_death )
ShutdownGamescope();
});
@@ -10040,6 +10165,14 @@
@@ -10057,6 +10207,14 @@ steamcompmgr_main(int argc, char **argv)

if ( g_bRun == false )
{
Expand All @@ -186,9 +254,11 @@ had an override surface is never blanked.
break;
}

diff --git a/src/steamcompmgr_shared.hpp b/src/steamcompmgr_shared.hpp
index c344d10..c8d8da5 100644
--- a/src/steamcompmgr_shared.hpp
+++ b/src/steamcompmgr_shared.hpp
@@ -161,6 +161,12 @@
@@ -161,6 +161,19 @@ struct steamcompmgr_win_t {
bool unlockedForFrameCallback = false;
bool receivedDoneCommit = false;

Expand All @@ -197,26 +267,14 @@ had an override surface is never blanked.
+ // window at some point. Only ever set by that coming into existence, so a
+ // session that never opens a game/second window can never be blanked.
+ bool bSessionSawGameWindow = false;
+
+ // gamenative: the newest done commit this window had when the override
+ // surface was first seen bound. Everything done up to then is what the
+ // compositor had before the game's content existed -- wine's desktop -- and
+ // any commit newer than it, from either surface, is content that arrived
+ // after. The blind at startup lifts on the first such commit.
+ uint64_t ulSessionOverrideBoundCommitID = 0;
+
std::shared_ptr<std::string> engineName;

std::vector< gamescope::Rc<commit_t> > commit_queue;
--- a/src/rendervulkan.cpp
+++ b/src/rendervulkan.cpp
@@ -3189,9 +3189,13 @@

gamescope::OwningRc<CVulkanTexture> vulkan_create_debug_blank_texture()
{
- // To match Steam's scaling, which is capped at 1080p
- int width = std::min<int>( g_nOutputWidth, 1920 );
- int height = std::min<int>( g_nOutputHeight, 1080 );
+ // A33: this texture is the session-end blank, painted as one full-output
+ // layer. Its extent must be the output's, not Steam's 1080p overlay cap:
+ // a capped extent is not screen size, which forces the composite path and
+ // leaves the blank's coverage resting on the shader's out-of-extent black
+ // fill instead of on the layer's own geometry.
+ int width = g_nOutputWidth;
+ int height = g_nOutputHeight;

return vulkan_create_flat_texture( width, height, 0, 0, 0, 0 );
}
Loading